/*
Class: Overlay

Element Structure:
	* body
		* container
			* closeButton
			* decoratedElement
				* element
		* background
		* ...

Example:
	>Overlay.open('myElementID');
	
	>var overlay = new Overlay(myElement, {modal: true});
	>overlay.open();
*/

var Overlay = new Class({
    Implements: Options,

    options: {
        width: null,
        height: null,
        modal: false,
        relative: true,
        maxwidth: null,
        maxheight: null
    },


    /*
    Method: initialize
	
	Arguments:
    el:*Elememt*
    options (optional) - {width, height, modal, src, relative}
    */
    initialize: function(el, options) {
        this.currentImage = 0;
        
		el = $(el);
        this.setOptions(options);

        //Properties
        this.isOpen = false;
        this.element = null;
        this.decoratedElement = null;
        this.background = new Element('div');
        this.container = new Element('div');
        this.closeButton = new Element('a', { text: 'close' });
        this.nextButton = new Element('a', { text: 'next' });
        this.prevButton = new Element('a', { text: 'prev' });

        //Init
        this.background.addClass('overlay_background');
        this.container.addClass('overlay_container');
        this.closeButton.addClass('close');
        this.nextButton.addClass('overlay_next');
        this.prevButton.addClass('overlay_prev');

        //events
        this.closeButton.addEvent('click', this.close.bind(this));
        this.background.addEvent('click', this.close.bind(this));
        this.container.addEvent('mousedown', this.handleMouseDown.bind(this));
        this.container.addEvent('mousemove', this.handleMouseMove.bind(this));
        this.nextButton.addEvent('click', this.nextImage.bind(this));
        this.prevButton.addEvent('click', this.prevImage.bind(this));
        if (document.addEvent) {
            document.addEvent('keydown', this.handleKeyEvent.bind(this));
        }
        
        //default styles
        this.container.setStyles({
            position: 'absolute'
        });

        //create loader
        this.loaderElement = new Element('div').addClass('loader');

        //set element
        if (!el)
            el = new Element('div');

        this.setElement(el);
    },


    /*
    Method: open
		
	Returns:
    self
    */
    open: function() {
        if (this.isOpen)
            return;

        //set initial position
        var startRect = this.getSourceCoordinates();
        this.container.setStyles(startRect);

        //set initial styles
        this.container.setStyles({
            opacity: .3
        });
        this.decoratedElement.setStyles({
            opacity: 0
        });
        this.closeButton.setStyles({
            opacity: 0
        });
        this.nextButton.setStyles({
            opacity: 0
        });
        this.prevButton.setStyles({
            opacity: 0
        });

        //animation
        this.bringToFront();
        this.updateSize();

        //inject
        this.decoratedElement.inject(this.container);
        if (this.options.modal)
            this.background.injectTop(document.body);
        this.container.injectTop(document.body);
        this.closeButton.injectTop(this.container);
        this.nextButton.injectTop(this.container);
        this.prevButton.injectTop(this.container);

        this.isOpen = true;

        return this;
    },


    /*
    Method: loading
		
	Returns:
    self
    */
    loading: function() {
        this.setElement(this.loaderElement);
        return this;
    },

    /*
    Method: close
	
	Returns:
    self
    */
    close: function() {
        if (this.isOpen == false)
            return this;

        this.isOpen = false;
        this.background.dispose();
        this.container.set('morph', { duration: 200 });
        this.container.get('morph').addEvent('complete', (function() {
            this.container.dispose();
            this.decoratedElement.dispose();
        }).bind(this));

        this.container.morph({
            opacity: 0
        });

        return this;
    },


    /*
    Method: bringToFront
		
	Returns:
    self
    */
    bringToFront: function() {
        //get highest z-index
        var zIndex = 0;
        $$('*').each(function(el) {
            zIndex = Math.max(parseInt(el.getStyle('z-index')) || 0, zIndex);
        });

        //apply z-index
        if (this.background)
            this.background.setStyle('zIndex', zIndex + 1);
        this.container.setStyle('zIndex', zIndex + 2);
        this.closeButton.setStyle('zIndex', zIndex + 3);
        this.nextButton.setStyle('zIndex', zIndex + 3);
        this.prevButton.setStyle('zIndex', zIndex + 3);

        return this;
    },


    /*
    Method: getTargetSize
    Returns the target size of the current element.
	
	Returns:
    {x, y}
    */
    getTargetSize: function() {
        var hidden = new Element('div');
        var container = new Element('div');

        var width = this.options.width;
        var height = this.options.height;
        var maxwidth = this.options.maxwidth;
        var maxheight = this.options.maxheight;

        hidden.setStyle('display', 'none');
        container.setStyles({
            'width': width,
            'height': height,
            'max-width': maxwidth,
            'max-height': maxheight,
            'position': 'absolute',
            'visibility': 'hidden'
        });

        this.decoratedElement.injectInside(container);
        container.injectBottom(document.body);

        size = container.getSize();

        this.decoratedElement.dispose();
        container.destroy();

        return size;
    },


    /*
    Method: setElement
    Sets the given element as the content for the overlay. If the overlay
    is currently open, it transitions to the new content with an animation.
		
	Arguments:
    el:*Element*
	
	Returns:
    self
    */
    setElement: function(el) {
        if (!this.isOpen) {
            this.element = $(el);
            this.decoratedElement = this.decorate(this.element);
        } else {
            //fade out contents
            var duration = this.decoratedElement.getStyle('opacity') / 1 * 300;
            this.decoratedElement.set('morph', { duration: duration });
            this.decoratedElement.morph({ opacity: 0 });

            //change
            (function() {
                this.decoratedElement.dispose();
                this.element = el;
                this.decoratedElement = this.decorate(this.element);
                this.decoratedElement.setStyle('opacity', 0);
                this.updateSize();
                this.decoratedElement.inject(this.container);
            }).bind(this).delay(duration);
        }

        if (this.timerId)
            window.clearTimeout(this.timerId);
        return this;
    },


    /*
    Method: setImage
    Sets the given image as the content for the overlay. Shows a 
    Preloader while loading the image.
		
	Arguments:
    uri:*string* - image uri
	
	Returns:
    self
    */
    setImage: function(uri) {
        this.setElement(this.loaderElement);

        var handleLoad = function(e) {
            this.setElement(image)
        }
        var handleError = function(e) {
            this.close();
        }

        var image = new Asset.image(uri, {
            'onload': handleLoad.bind(this),
            'onerror': handleError.bind(this),
            'style': 'display:block;'
        });

        return this;
    },

    /*
    Method: setImages
    Sets the image given in the firstUri as the content of the overlay. Identifies, collects the rest of the
    gallery's images and adds them to the imageArray. The gallery images are identified by the rel attribute with the 
    value of the parameter 'rel'
    
    Arguments:
    firstUri:*string* - image uri of the first image to display
    rel:*string* - rel attribute value of the rest of the images in the gallery
	
	Returns:
    self
    */
    setImages: function(firstUri, rel) {
        this.setElement(this.loaderElement);
        this.imageArray = [];

        var handleComplete = function() {
            this.setElement(this.imageArray[this.currentImage]);
        }
        var handleError = function(e) {
            this.close();
        }
        var listOfImages = [];
		var elPosition = 0;
        var elements = $$('a[rel=' + rel + ']');
        elements.each(function(el) {
            var href = el.get('href');
            if (href == firstUri) {
				elPosition = listOfImages.length;
            }
                listOfImages.push(el.get('href'));
        });
		this.currentImage = elPosition;
        this.imageArray = new Asset.images(listOfImages, {
            onComplete: handleComplete.bind(this),
            'onerror': handleError.bind(this),
            'style': 'display:block;'
        });

        return this;
    },

    /*
    Method: nextImage
    Displays the image next to the currently displayed image in the imageArray
    */
    nextImage: function() {
        if (this.currentImage + 1 >= this.imageArray.length)
            return;

        this.container.removeEvents('mousemove');
        this.currentImage++;
        if (this.timerId) {
            window.clearTimeout(this.timerId);
            this.timerId = 0;
        }
        this.setElement(this.imageArray[this.currentImage]);
        this.container.addEvent('mousemove', this.handleMouseMove.bind(this));
    },

    /*
    Method: prevImage
    Displays the image previous to the currently displayed image in the imageArray
    */
    prevImage: function() {
        if (this.currentImage <= 0)
            return;

        this.container.removeEvents('mousemove');
        this.currentImage--;
        if (this.timerId) {
            window.clearTimeout(this.timerId);
            this.timerId = 0;
        }
        this.setElement(this.imageArray[this.currentImage]);
        this.container.addEvent('mousemove', this.handleMouseMove.bind(this));
    },

    /*
    Method: updateSize
		
	Returns:
    self
    */
    updateSize: function() {
        var size = this.getTargetSize();

        if (this.options.relative) {
            var sourceCoordinates = this.getSourceCoordinates();
        } else {
            var sourceCoordinates = this.getStageCenterCoordinates();
        }

        var rect = this.getRelativeCoordinates(sourceCoordinates, size);

        this.setCoordinates(rect.left, rect.top, rect.width, rect.height);

        return this;
    },


    /*
    Method: getStageCenterCoordinates
	
	Returns:
    {top, left, width, height}
    */
    getStageCenterCoordinates: function() {
        var stage = $(document.body).getCoordinates();
        var scroll = window.getScroll();
        var rect = {
            width: 0,
            height: 0,
            left: scroll.x + stage.width * .5,
            top: scroll.y + stage.height * .5
        };

        return rect;
    },


    /*
    Method: getSourceCoordinates
	
	Returns:
    {top, left, width, height}
    */
    getSourceCoordinates: function() {
        var rect = null;

        if (this.isOpen) {
            rect = this.container.getCoordinates();
        } else if (this.options.src) {
            rect = this.options.src.getCoordinates();
        } else {
            rect = this.getStageCenterCoordinates();
        }
        return rect;
    },


    /*
    Method: setCoordinates
    Changes the coordinates of the overlay with an animation.
		
	Returns:
    self
    */
    setCoordinates: function(x, y, width, height) {
        this.container.set('morph', { duration: 300 });
        this.container.morph({
            left: x,
            top: y,
            width: width,
            height: height,
            opacity: 1
        });

        this.decoratedElement.set('morph', { duration: 300 });
        this.decoratedElement.morph.delay(300, this.decoratedElement, {
            opacity: 1,
            duration: 300
        });

        this.closeButton.set('morph', { duration: 300 });
        this.closeButton.morph.delay(300, this.closeButton, {
            opacity: 1
        });

        return this;
    },


    /*
    Method: getRelativeCoordinates
    Get coordinates for a rectangle of "size", centered relative to "sourceRect".
	
	Returns:
    {left, top, width, height}
    */
    getRelativeCoordinates: function(sourceRect, size) {
        var stage = $(document.body).getCoordinates();
        var scroll = window.getScroll();

        var rect = {
            width: size.x,
            height: size.y,
            left: (sourceRect.left + sourceRect.width * .5) - size.x * .5,
            top: (sourceRect.top + sourceRect.height * .5) - size.y * .5
        };

        rect.left = Math.max(scroll.x + 20, Math.min(scroll.x + (stage.width - rect.width - 20), rect.left));
        rect.top = Math.max(scroll.y + 20, Math.min(scroll.y + (stage.height - rect.height - 20), rect.top));

        return rect;
    },


    /*
    Method: decorate
	
	Returns:
    *Element* - decorated element
    */
    decorate: function(el) {
        el = $(el);
        if (this.options.maxwidth || this.options.maxheight)
        {
            if (this.options.maxwidth && this.options.maxwidth < el.width)
                el.style.maxWidth = this.options.maxwidth + 'px';
            if (this.options.maxheight && this.options.maxheight < el.height)
                el.style.maxHeight = this.options.maxheight + 'px';
            el.removeAttribute('width');
            el.removeAttribute('height');
        }
        return new Element('div').addClass('overlay_shadow').grab(el);
    },

    /*
    Method: fadeInNavigation
    Morphs the opacity of the next and previous buttons to 1
    */
    fadeInNavigation: function() {
        if (this.imageArray && this.imageArray.length > 1) {
            this.nextButton.set('morph', { duration: 300 });
            this.nextButton.morph.delay(300, this.nextButton, {
                opacity: 0.8
            });
            this.prevButton.set('morph', { duration: 300 });
            this.prevButton.morph.delay(300, this.prevButton, {
                opacity: 0.8
            });
        }
    },

    /*
    Method: fadeOutNavigation
    Morphs the opacity of the next and previous buttons to 0
    
    Arguments:
    ov:*Overlay* - the overlay containing the navigation to hide
    
    Info: Requires the parameter ov because this function is used as an event listener of timeout and this is
    not the Overlay in the context of the timeout
    */
    fadeOutNavigation: function(ov) {
        if (ov.timerId) //TODO: IE ov null (sometimes)
            ov.timerId = 0;
        ov.nextButton.set('morph', { duration: 300 });
        ov.nextButton.morph.delay(300, ov.nextButton, {
            opacity: 0
        });
        ov.prevButton.set('morph', { duration: 300 });
        ov.prevButton.morph.delay(300, ov.prevButton, {
            opacity: 0
        });
    },

    /*
    Section: Event Handler
    */

    /*
    Clears ticking timers, starts a new one and fades in the navigation if necessary
    */
    handleMouseMove: function(e) {
        if (this.timerId) {
            window.clearTimeout(this.timerId);
        }
        else {
            this.fadeInNavigation();
        }
        this.timerId = window.setTimeout(this.fadeOutNavigation.pass(this), 1000);
    },

    handleMouseDown: function(e) {
        this.bringToFront();
    },
    
    handleKeyEvent: function(e) {
        var keyCode;
        if (e.event && e.event.keyCode) {
            keyCode = e.event.keyCode;
        }
        if (keyCode == 37) {
            // left arrow
            this.prevImage();
        }
        else if (keyCode == 39) {
            // right arrow
            this.nextImage();
        }
        else if (keyCode == 27) {
            // escape
            this.close();
        }
    }
});


/*
Section: Shortcuts 
*/
Overlay.extend({
    open: function(el, options) {
        return new Overlay(el, options).open();
    },

    /*
    Method: assign
    attaches a Overlay.open click handler to every element matching the *param to
	
	Example:
    Overlay.assign($$('a[class=boxed]')); //adds the click handler to all anchors with 'class=boxed' attribute
    */
    assign: function(to, options) {
        to = $$(to);

        to.each(function(el) {
            el.addEvent('click', function(e) {
                e.preventDefault();

                var overlay = new Overlay(null, options).loading().open();

                var handleSuccess = function(nodeList, elements, html) {
                    var el = new Element('div').set('html', html);
                    overlay.setElement(el);
                }

                var handleFailure = function(xhr) {
                    overlay.close();
                }

                var request = new Request.HTML({ url: el.get('href'), method: 'get' })
                request.addEvent('success', handleSuccess);
                request.addEvent('failure', handleFailure);
                request.send();
            });
        });
    },

    /*
    Method: assignImage
    - attaches a Overlay.open click handler to every element matching the *param to, opening the 'href' uri as an image
    - if matching element has a 'rel' attribute, this attribute is used to identify the rest of the members of the gallery (a elements with href to image)
	
	Example:
    Overlay.assignImage($$('a[class=boxedImage]')); //adds the click handler to all anchors with 'class=boxedImage' attribute
    */
    assignImage: function(to, options) {
        to = $$(to);

        to.each(function(el) {
            el.addEvent('click', function(e) {
                e.preventDefault();

                var overlay = new Overlay(null, options).loading().open();
                if (el.get('rel')) {
                    // image gallery
                    overlay.setImages(el.get('href'), el.get('rel'));
                }
                else {
                    overlay.setImage(el.get('href'));
                }
            });
        });
    }
});
