/**
 * This is the MiniBox source file.
 * MiniBox is yet another lightbox clone. Aside of displaying
 * images, almost anything can be displayed inside the MiniBox.
 * 
 * @copyright 2009 Phase4 Communications GmbH
 * @author Jean-Bernard Valentaten <valentaten@phase4.de>
 * @version 1.0
 * @package MiniBox
 */

/**
 * MiniBox
 * 
 * The class that makes it all happen.
 * Links that shall trigger the MiniBox must have
 * a rel-Attribute that contains the string 'minibox' only.
 * The item (its id) to display within the MiniBox is then 
 * stripped from the href-Attribute.
 * Example:
 * <code>
 *   <a href="#bar" rel="minibox">foo</a>
 * </code>
 * 
 * Options:
 * @param {int} duration the duration of the container animation in msec (default: 500)
 * @param {bool} hideFlash Whether Flash movies shall be hidden when MiniBox is 
 *                         open (default: true)
 * @param {float} overlayOpacity The opacity of the overlay (default: 0.6)
 * @param {String,Fx.Transition} transition The transition to use when animating 
 *                                          container (default: 'sine:in:out')
 * 
 * Events:
 * @param {Function} close Fired when minibox is closed
 * @param {Function} complete Fired when all steps have been completed
 * @param {Function} start Fired before the first step
 * 
 * @requires MooTools v1.2.x
 */
var MiniBox = new Class({
    /*
     * Inheritance
     */
    
    /**
     * Implement Events and Options
     */
    Implements: [Options, Events],
    
    /*
     * Properties
     */
    
    /**
     * Translates numeric steps to callbacks
     * 
     * @type {Object}
     * @private
     */
    STEPS: {
        0: 'start',
        1: 'displayOverlay',
        2: 'displayContainer',
        3: 'animateContainer',
        4: 'showContent',
        5: 'complete'
    },
    
    /**
     * Stores our default options
     * 
     * @type {Object}
     * @private
     */
    options: {
        /* Options */
        duration: 500,
        hideFlash: true,
        overlayOpacity: 0.6,
        transition: 'sine:in:out',
        
        /* Events */
        onClose: $empty,
        onComplete: $empty,
        onStart: $empty
    },
    
    /*
     * Methods
     */
     
    /**
     * Constructor
     * 
     * @constructor
     * @class
     * 
     * @param {Object} options The options as described above
     */
    initialize: function(options) {
        //init properties
        this.currentLink = null;
        this.isOpen    = false;
        
        //init options
        this.setOptions(options);
        
        //setup
        this._initLinks();
        this._initDocument();
        
        //finally we add a key-listener to document and a few listeners to window
        document.addEvent('keydown', this.keyListener.bindWithEvent(this));
        window.addEvent('scroll', this.reposition.bind(this));
        window.addEvent('resize', this.reposition.bind(this));
    },
    
    /**
     * Returns the contents actual size, even when content is hidden
     * 
     * @return {Object} The contents size
     */
    _getContentSize: function() {
        //init vars
        var content = $(this.currentLink.retrieve('id'));
        var size;
        var styles = {
            visibility: content.style.visibility,
            display: content.style.display
        };
        
        this._swfSafeSetStyles(content, { visibility: 'hidden', display: '' });
        size = content.getSize();
        this._swfSafeSetStyles(content, styles);
        
        return size;
    },
    
    /**
     * Initializes the document
     * 
     * @private
     */
    _initDocument: function() {
        //init our elements
        this.bottom = new Element('div', {
            id: 'mbBottom'
        });
        this.closeLink = new Element('a', {
            id: 'mbCloseLink',
            html: '&nbsp;'
        });
        this.center = new Element('div', {
            id: 'mbCenter'
        });
        this.container = new Element('div', {
            id: 'mbContainer'
        }).setStyle('display', 'none');
        this.overlay = new Element('div', {
            id: 'mbOverlay',
            styles: {
                opacity: this.options.overlayOpacity,
                display: 'none'
            }
        });
        
        //position them in DOM
        this.bottom.grab(this.closeLink);
        this.container.adopt(this.center, this.bottom);
        this.overlay.inject(document.body);
        this.container.inject(document.body);
        
        //add a listener to close link and overlay
        this.closeLink.addEvent('click', this.close.bind(this));
        this.overlay.addEvent('click', this.close.bind(this));
    },
    
    /**
     * Initializes the documents links
     * 
     * @private
     */
    _initLinks: function() {
        //grab all links that contain our defined relation and a valid id in href
        this.links = $$('a').filter(function(link) {
            //init vars
            var id  = (link.get('href')|| '').replace(/^#/, '');
            var rel = link.get('rel');
            
            //if id is valid, we store it for later use
            if (!!$(id)) link.store('id', id);
            
            //return whether relation and id are valid
            return (rel=='minibox' && !!$(id));
        });
        
        //initialize the links by adding event listeners
        this.links.each(function(link) {
            //init vars
            var elem = $(link.retrieve('id'));
            
            //add an event listener
            link.addEvent('click', this.click.bindWithEvent(this));
        }, this);
    },
    
    /**
     * Returns whether a specified link is one of our stored links
     * 
     * @param {Element} link The link that shall be inspected
     * @return {bool} True if link is one of ours, false otherwise
     * @private
     */
    _isStoredLink: function(link) {
        return !!(this.links.filter(function(el) {
            return el===link;
        }).length);
    },
    
    /**
     * This is a MSIE swf-safe style setter.
     * 
     * @param {Element} element The element the styles shall be applied to
     * @param {Object} styles The styles that shall be set
     * @private
     */
    _swfSafeSetStyles: function(element, styles) {
        //is extended method is supported, we use it
        if ($type(element.setStyles)=='function') {
            element.setStyles(styles);
            return;
        }
        
        //still here, so we have to iterate
        $H(styles).each(function(value, key) {
            element.style[key] = value;
        });
    },
    
    /**
     * Our third step animates the container to fit our links element size
     * 
     * @param {int} nextStep The index of our next step
     */
    animateContainer: function(nextStep) {
        //init vars
        var content = $(this.currentLink.retrieve('id'));
        var scrollOffset = window.getScroll();
        var size = this._getContentSize();
        
        //add bottoms height and its vertical margins
        size.y += this.bottom.getSize().y 
                + parseInt(this.bottom.getStyle('marginTop'))
                + parseInt(this.bottom.getStyle('marginBottom'));
        
        //setup containers morph
        this.container.set('morph', {
            duration: this.options.duration,
            transition: this.options.transition,
            unit: 'px',
            onComplete: this.step.bind(this, nextStep)
        });
        
        //and morph it
        this.container.morph({
            height: size.y,
            width: size.x,
            marginTop: Math.floor(scrollOffset.y - (size.y / 2)),
            marginLeft: Math.floor(scrollOffset.x - (size.x / 2))
        });
    },
    
    /**
     * Event handler for clicked links
     * 
     * @param {Event} evt The event that triggered us
     */
    click: function(evt) {
        //init vars
        var target = $(evt.target);
        
        //make sure target is a link
        if (target.get('tag')!='a') target = target.getParent('a');
        
        //if target is not one of our links, we terminate
        //otherwise we stop event propagation and prevent default
        if (!this._isStoredLink(target)) {
            return;
        } else {
            evt.stop();
        }
        
        //if we came here, we have a valid link, so we store it
        //and start stepping through our display procedures.
        this.currentLink = target;
        this.step(0);
        
        //return false to prevent a link from being executed
        return false;
    },
    
    /**
     * Closes the minibox
     */
    close: function() {
        //reset container
        this.container.setStyles({
            display: 'none',
            width: '',
            height: '',
            marginLeft: '',
            marginTop: ''
        });
        
        //hide overlay
        this.overlay.setStyle('display', 'none');
        
        //empty center by placing current contents into document body
        this.center.getChildren().each(function(child) {
            //if this is MSIE, we can't use extended methods
            this._swfSafeSetStyles(child, { display: 'none' });
            $(document.body).adopt(child);
        }, this);
                
        //if we were requested to hide flash movies, we restore their states
        $$('object', 'embed').each(function(obj) {
            this._swfSafeSetStyles(obj, { visibility: 'visible' });   //for IE6+7 we mustn't use extended methods
        }, this);
        
        //set open to false and return false to prevent links defaults
        this.isOpen = false;
        this.fireEvent('close');
        return false;
    },
    
    /**
     * Called when all steps have gone through.
     * Fires the complete event
     */
    complete: function() {
        this.isOpen = true;
        this.reposition();
        this.fireEvent('complete');
    },
    
    /**
     * Our second step displays the container in minimized form.
     * As a side effect, we prepare the content to be sure to get
     * it's correct size
     * 
     * @param {int} nextStep The index of our next step
     */
    displayContainer: function(nextStep) {
        //show container and proceed to next step
        this.container.setStyle('display', 'block');
        this.step(nextStep);
    },
    
    /**
     * Our first step displays the overlay
     * 
     * @param {int} nextStep The index of our next step
     */
    displayOverlay: function(nextStep) {
        //restyle overlay and make it visible
        this.overlay.setStyles({
            display: 'block',
            height: window.getSize().y + 'px'   //this helps IE6 to determine the correct height
        });
        this.step(nextStep);
    },
    
    /**
     * Reacts on key-events
     * 
     * @param {Event} evt
     */
    keyListener: function(evt) {
        //let's see what key was pressed
        switch (evt.key) {
            case 'esc':
                if (this.isOpen) this.close();
                break;
            default:
                //nop
        }
    },
    
    /**
     * Repositions our overlay and our container when
     * window is changed
     */
    reposition: function() {
        //init vars
        var containerSize = this.container.getSize();
        var scrollOffset = window.getScroll(); 
        
        //reposition overlay
        this.overlay.setStyles({
            height: window.getSize().y + 'px',  //this helps IE6 to determine the correct height
            left: scrollOffset.x  + 'px',
            top: scrollOffset.y + 'px'
        });
        
        //reposition container
        this.container.setStyles({
            marginLeft: Math.floor(scrollOffset.x - (containerSize.x / 2)) + 'px',
            marginTop: Math.floor(scrollOffset.y - (containerSize.y / 2)) + 'px'
        }); 
    },
    
    /**
     * Our fourth step finally shows the content in our container
     * and fires the complete event
     * 
     * @param {int} nextStep The index of our next step
     */
    showContent: function(nextStep) {
        //init vars
        var content = $(this.currentLink.retrieve('id'));
        
        //show content
        this._swfSafeSetStyles(content, { visibility: '', display: '' });
        
        //and show flash movies that are in content
        $$(content.getChildren('object'), content.getChildren('embed')).each(function(obj) {
            this._swfSafeSetStyles(obj, { visibility: 'visible' });
        }, this);
        
        //and
        this.step(nextStep);
    },
    
    /**
     * Starts the process.
     * Positions the element that shall be shown in center,
     * then fires start-event
     * 
     * @param {int} nextStep The next steps index 
     */
    start: function(nextStep) {
        //init vars
        var content = $(this.currentLink.retrieve('id'));
        
        //if we were requested to hide flash movies, we do so
        if (this.options.hideFlash) {
            $$('object', 'embed').each(function(obj) {
                this._swfSafeSetStyles(obj, { visibility: 'hidden' });
            }, this);
        }
        
        //grab content
        this.center.grab(content, 'top');
        
        //fire start-event and proceed to next step
        this.fireEvent('start');
        this.step(nextStep);
    },
    
    /**
     * Calls the requested step
     * 
     * @param {int} step The step that is to be displayed
     */
    step: function(step) {
        var callbackName = this.STEPS[step];
        
        //if we have a callback, we call it and go to next step
        if (callbackName) (this[callbackName].bind(this))(++step);
    }
});
