/**
 * Scope overrides copied from baseJS/prototype.
 * Useful in writing class-base JavaScript for keeping the parent Object as the scope.
 */
$.extend(Function.prototype, {
    /**
     * Override scope of a function.
     * @param oScope     {Object}      Object to override the scope of the function.
     * @param arguments  {*}           Any additional arguments to pass.
     */
    use: function() {
        var method = this, args = Array.prototype.slice.call(arguments), object = args.shift();
        return function() {
            return method.apply(object, args.concat(Array.prototype.slice.call(arguments)));
        }
    },
    /**
     * Override scope of a callback function on an event.
     * @param oScope     {Object}      Object to override the scope of the function.
     * @param arguments  {*}           Any additional arguments to pass.
     */
    useEL: function() {
        var method = this, args = Array.prototype.slice.call(arguments), object = args.shift();
        return function(event) {
            return method.apply(object, [event || window.event].concat(Array.prototype.slice.call(arguments)));
        }
    }
});

$.extend(jQuery.Event.prototype, {
    /**
     * Adding a bucket event to jQuery's Event class to stopPropagation and preventDefault in a shorter method call.
     */
    stop: function() {
        this.stopPropagation();
        this.preventDefault();
    }
});


/**
 * Human-readable Date strings.
 */
$.extend(Date, {
    format: { 
        months: { 
            full: [
                'January',
                'February',
                'March',
                'April',
                'May',
                'June',
                'July',
                'August',
                'September',
                'October',
                'November',
                'December'
            ]
        }
    }
});

/**
 * The main class for running a WebSlide Show.
 * This is initialized once on DOM load via WebSlide.init();
 */
var WebSlide = function() {
    // jQuery caches elements, but these are handier.
    this.els = {};
    for(var a in WebSlide.elements) {
        this.els[a] = $(WebSlide.elements[a]);
    }
    
    this.config = {};
    $.extend(this.config, WebSlide.defaults, wsConfig);
    
    // this.slides will be reused to hold the actual Slide objects.
    this.slides = [];
    
    this.config.alignment = this.config.alignment.split(/\s/);
    
    // IE can't access $('title'), so we fall back on the old-fashioned method to set the title
    document.title += ': ' + this.config.client + ' - ' + this.config.project;
    
    // Set the text of the intro slide per the config file
    if(this.config.client) {
        this.els.client.text(this.config.client);
    } else {
        this.els.client.hide();
        this.els.clientTitle.hide();
    }
    
    if(this.config.project) {
        this.els.project.text(this.config.project);
    } else {
        this.els.project.hide();
        this.els.projectTitle.hide();
    }

    this.els.date.text(
        Date.format.months.full[this.config.date.getMonth()] + ' ' + 
        this.config.date.getDate() +', ' + 
        this.config.date.getFullYear()
    );
    
    // Hide the details so that we can fade it in nicely after showIntro is called.
    this.els.details.addClass('intro').hide();
    
    // Check to see if the config requires a logo.
    // Attempt to load and display the intro slide.
    if(!(/^\x*$/.test(this.config.logo))) {
        var img = new Image();
        
        img.onload = function() {
            this.els.logo.append(img);
            this.showIntro();
        }.use(this);
        
        img.onerror = function() {
            this.showIntro();
        }.use(this);
        
        img.src = this.config.server+this.config.logo;
    } else {
        this.showIntro();
    }
    
    this.navSide = this.getNavSide();

    this.els.document.bind('loadingStart', this.showLoading.useEL(this));
    this.els.document.bind('loadingComplete loadingError', this.hideLoading.useEL(this));
};

/**
 * Static variables and methods for WebSlide.
 */
$.extend(WebSlide, {
    elements: {
        container: '#wsContainer',
        details: '#wsDetails',
        logo: '#wsLogo',
        clientTitle: '#wsClientTitle',
        client: '#wsClient',
        projectTitle: '#wsProjectTitle',
        project: '#wsProject',
        dateTitle: '#wsDateTitle',
        date: '#wsDate',
        authForm: '#wsAuth',
        password: '#wsPassword',
        instructions: '#wsInstructions',
        slideContent: '#wsContent',
        loading: '#wsLoading',
        navbar: '#wsNavbar',
        navtab: '#wsNavtab',
        navtablink: '#wsNavtabLink',
        navtabTextOpen: '#wsNavtabTextOpen',
        navtabTextClosed: '#wsNavtabTextClosed',
        count: '#wsCount',
        title: '#wsSlideTitle',
        desc: '#wsSlideDesc',
        list: '#wsList',
        print: '#wsPrintLink',
        help: '#wsHelpLink',
        helpDialog: '#wsHelpDialog',
        helpClose: '#wsHelpClose',
        document: document,
        window: window
    },
    defaults: {
        client:         '',
        project:        '',
        date:           new Date(),
        background:     '#FFF',
        alignment:      'center top',
        logo:           '',
        needsAuth:      false,
        autoHideNav:    false,
        server:         '',
        passwordFailureText: 'Password incorrect. Please try again.',
        passwordInstructionsText: 'Please enter the password to continue.'
    },
    /**
     * Initialize a new instance of WebSlide.
     */
    init: function() {
        if(!WebSlide.instance) {
            WebSlide.instance = new WebSlide();
        }
    }
});
$.extend(WebSlide.prototype, {
    /**
     * Set the position of and show the introduction slide
     */
    showIntro: function() {
        this.els.window.bind('resize', this.setPosition.useEL(this));
        this.setPosition(true);
        this.hideLoading();
                
        this.els.navbar.hide();
        this.hideNav(null, 1, 1);
        this.els.details.fadeIn(1000);
        
        // Add all of the slide objects before progressing onward.
        this.addSlides();
        
        // Require password if demanded.
        if(this.config.needsAuth) {
            this.els.instructions.text(this.config.passwordInstructionsText);
            this.els.password.focus();
            
            // Use lazy auth on form submission.
            this.els.authForm.bind('submit', this.authenticate.useEL(this));
        } else {
            this.els.authForm.hide();
            
            // Prepend the server location and assume the directory for slides is 'images/'.
            var selfServer = this.config.server;
            $.each(this.slides, function() {
                this.src = selfServer+'images/'+this.file;
            });
            
            // Allow any key or click to hide the intro screen.
            this.els.document.bind('keyup.intro click.intro', this.hideIntro.useEL(this));
        }
    },
    /**
     * Hide the introduction slide and advance on to the first slide in the show.
     */
    hideIntro: function() { 
        this.els.document.unbind('keyup.intro click.intro')
        
        this.els.navbar.fadeIn(500);
        
        var finishHide = function() {
            // Advance to the first slide, show (and hide) the nav in succession.
            var go = function() {
                this.advance(null, 0, function() { 
                    this.showNav(null, 1200);
                    
                    if(this.config.autoHideNav) {
                        this.hideNav(null, 1000, 1800);
                    }
            
                    // Wait until everything is done before appending the main WebSlide events.
                    this.appendEvents();
                }.use(this));
            }.use(this);
            
            // Wait for the first slide to be preloaded before displaying it.
            this.preload(0, go);
        }.use(this);
        
        this.els.details.effect('puff', {}, 500, function() {
            $('body').animate({ backgroundColor: this.config.background }, 500, 'linear', finishHide);
        }.use(this));
    },
    /**
     * Lazy Authentication. Preload returns an error if it couldn't find the image.
     * @param event     {Event}         Used to stop the form from actually submitting.
     */
    authenticate: function(event) {
        event.stop();
        
        // Check for the image at a URL based on the password.
        // Make the src object for each slide the location+file.
        var slideLocation = this.config.server+this.els.password.attr('value')+'/';
        $.each(this.slides, function() {
            this.src = slideLocation+this.file;
        });
        
        // On error, report the failure and give it a little shake.
        var authError = function() {
            this.els.instructions.addClass('error');
            this.els.instructions.effect('shake', {times: 2, distance: 5}, 50);
            this.els.instructions.text(this.config.passwordFailureText);
        }.use(this);
        
        // Attempt preloading with the hideIntro, authError callbacks.
        this.preload(0, this.hideIntro.use(this), authError.use(this))
    },
    /**
     * Add the slides from the config as Slide objects into a new array of objects.
     */
    addSlides: function() {
        // Can't employ .use() here because of jQuery's internal scoping on .each()
        var self = this;
        $.each(this.config.slides, function() {
            self.slides.push(new Slide(this));
            self.els.list.append('<li><a href="#'+self.slides.length+'">'+self.slides.length+'</a></li>');
        });
        this.els.slideTiles = $(WebSlide.elements.list+' li');
    },
    /**
     * Advance the show to a new slide (forward or backwards).
     * @param event     {Event}         If a link was clicked or left/right arrow key was pressed.
     * @param slide     {Number}        Force to an exact slide in the array.
     * @param callback  {Function}      Run a function after the slide is finished showing.
     */
    advance: function(event, slide, callback) {
        // Make sure someone isn't dragging around while trying to switch slides.
        if(this.dragging) { return; }
        
        var next = slide+1;
        var slide = slide;
        // Dummy out the callback if it doesn't exist.
        var callback = (callback) ? callback : function() {};

        if(event) {
            // Check for left/right arrow key presses.
            switch(event.keyCode) {
                case 39: // Right
                case 63235: // Safari 2 Right
                case 32: // Spacebar
                    slide = this.currentSlide+1;
                    if(slide < 0) { slide = this.slides.length-1; }
                    if(slide >= this.slides.length) { slide = 0; }
                    next = slide+1;
                    break;
                case 37: // Left
                case 63234: // Safari 2 Left
                    slide = this.currentSlide-1;
                    if(slide < 0) { slide = this.slides.length-1; }
                    if(slide >= this.slides.length) { slide = 0; }
                    next = slide-1; 
                    break;
            }
            // Kill the default if user clicked a slide tile button.
            if(event.target.nodeName.toLowerCase() === 'a' && event.type == 'click') {
                event.stop();
                    slide = event.target.hash.replace('#', '')-1;
                next = slide+1;
            }
        }
        // Kill out if there the slide doesn't exist.
        if(typeof slide != 'number') { return; }
        
        // Figure out if the next slide is out of the scope of the array.
        if(next < 0) { next = this.slides.length-1; }
        if(next >= this.slides.length) { next = 0; }
        
        this.els.document.trigger('slidechange', { slide: next });
 
        var go = function(rebind) {
            if(rebind) {
                // Rebind the key listeners if they aren't already bound.
                this.els.document.bind('keyup.advance', this.advance.use(this));
            }
            if(typeof this.currentSlide == 'number') {
                this.slides[this.currentSlide].hide();
            }
            this.appendSlide(this.slides[slide]);
            this.currentSlide = slide;
            this.els.count.text(this.currentSlide+1+' of '+this.slides.length);
            this.setPosition();
            
            // Run the callback before showing the slide and preloading the next.
            callback();

            this.currentSlideInfo();
            this.slides[slide].show();
            
            this.preload(next);
        }.use(this);
        
        // Preload the slide if it is not yet cached.
        // Otherwise, as long as it's valid and not the current slide, push it up.
        if(this.slides[slide] != 'undefined' && !this.slides[slide].cache) {
            // destroy the ability to keep moving forward if this slide hasn't been loaded
            this.els.document.unbind('keyup.advance');
            this.preload(slide, go.use(this, true))
        } else if(this.currentSlide == 'undefined' || slide != this.currentSlide) {
            go(); 
        }
    },
    /**
     * Bind the main navigation and UI events for the show.
     */
    appendEvents: function() {
        // Prevent the default window actions of scrolling left/right
        this.els.document.bind('keydown', this.preventArrows.useEL(this));
        
        // Left and right navigation keys
        this.els.document.bind('keyup.advance', this.advance.useEL(this));
        
        // Show and hide the nav bar
        this.els.document.bind('keyup.nav', this.toggleNav.useEL(this));
        this.els.navtab.bind('click', this.toggleNav.useEL(this));
        
        // Make the slides draggable
        this.els.document.bind('mousedown', this.startDrag.useEL(this)).bind('mouseup', this.endDrag.useEL(this));
        
        // Slide button numbers
        this.els.list.find('li').bind('click', this.advance.useEL(this));
        this.els.list.find('a').bind('mouseover', this.previewSlide.useEL(this)).bind('mouseout', this.unpreviewSlide.useEL(this));
        
        // Print link
        this.els.print.bind('click', this.readyPrint.useEL(this));
        
        // Help Dialog
        this.els.help.bind('click', this.toggleHelpDialog.useEL(this));
        this.els.helpClose.bind('click', this.hideHelpDialog.useEL(this));
        this.els.document.bind('keyup', this.toggleHelpDialog.useEL(this));
        
        // Prevent draggable images (browser function)
        this.els.slideContent.children().live('click mousedown', this.preventImgDrag.useEL(this));
        
        // Firefox tries to create a selection on window blur
        // On window focus, reset the selection to nothing
        this.els.window.bind('focus', function() { 
            if(window.getSelection) { window.getSelection().collapse($('body').get(0), 0); }
        });
    },
    /**
     * Stop the default browser behaviors for the left/right arrows so that they can be used for keyboard navigation.
     * @param event     {Event}
     */
    preventArrows: function(event) {
        switch(event.keyCode) {
            case 39: // Right
            case 63235: // Safari 2 Right
            case 37: // Left
            case 63234: // Safari 2 Left
            case 32: // Spacebar
                event.stop();
                return false;
            break;
        }
    },
    /**
     * Stop browsers from letting you drag images out of the browser to another application.
     * @param event     {Event}
     */
    preventImgDrag: function(event) {
        // prevent image dragging out of the browser
        event.stop();
        // prevent IE from dragging images
        document.ondragstart = function() {
            return false;
        };
    },
    /**
     * Preview the slide title in the designated slide title area.
     * @param event     {Event}     Used to find the correct slide element to preview.
     * @param slide     {String}    The key number of the slide in the slides array. Overrides event.
     */
    previewSlide: function(event, slide) {
        clearTimeout(this.previewTimeout);
        slide = (slide && slide != 'undefined') ? slide : $(event.target).get(0).hash.replace('#', '')-1;
        this.changeSlideInfo(slide);
    },
    /**
     * Revert to the current slide title and description.
     */
    unpreviewSlide: function() {
        this.previewTimeout = setTimeout(function() {
            this.currentSlideInfo();  
        }.use(this), 300);
    },
    /**
     * Reset the slide info elements to the current slide.
     */
    currentSlideInfo: function() {
        this.els.title.text(this.slides[this.currentSlide].title);
        this.els.desc.text(this.slides[this.currentSlide].description);
        
        // Add a class the the current slide for CSS themes.
        this.els.slideTiles.removeClass('current');
        $(this.els.slideTiles[this.currentSlide]).addClass('current');
    },
    /**
     * Set the slide info elements to a particular slide.
     * @param slide     {Number}        Number key of the slide to set as the slide info.
     */
    changeSlideInfo: function(slide) {
        this.els.title.text(this.slides[slide].title);
        this.els.desc.text(this.slides[slide].description);
    },
    /**
     * Tell the slide object to preload its image.
     */
    preload: function(key, callback, error) {
        this.slides[key].preload(callback, error);
    },
    /**
     * Add the slide to the current stack of slides actually in the HTML display.
     * @param slide     {String}        HTML of the slide.
     */
    appendSlide: function(slide) {
        // Make sure it's not already on the stage.
        if(!slide.cache.parentNode) {
            this.els.slideContent.prepend(slide.cache);
            // Titles are added for printing.
            this.els.slideContent.prepend('<h2>'+slide.title+'</h2>');
        }
    },
    /**
     * Report the window size and positioning instructions to the slide object.
     * Center the details information for in the window.
     */
    setPosition: function(forceDetails) {
        var docWidth = this.els.window.width();
        var docHeight = this.els.window.height();
        var topPos = Math.round((docHeight/2)-($(this.els.details).height()/2));
        var leftPos = Math.round((docWidth/2)-($(this.els.details).width()/2));
        
        // Tell the current slide to position itself.
        if(this.slides[this.currentSlide]) {
            var slide = this.slides[this.currentSlide];
            slide.setPosition(this.config.alignment, [docWidth, docHeight]);
        }
        
        // Center the intro slide if it's visible or forced.
        if(this.els.details.css('display') != 'none' || forceDetails == true) {
            this.els.details.css({ top: topPos+'px', left: leftPos+'px' });
        }
    },
    /**
     * Show the loading element.
     */
    showLoading: function() {
        this.els.loading.stop().fadeIn(200, function() {
            this.loading = true;
        }.use(this));
    },
    /**
     * Hide the loading element.
     */
    hideLoading: function() {
        this.els.loading.stop().fadeOut(200, function() {
            this.loading = false;
        }.use(this));
    },
    /**
     * Get the side that the navbar is attached to. Side in CSS must be set to -1px (or anything less than 0)
     * returns {String}     One of 'bottom', 'right', 'left', or 'top'
     */
    getNavSide: function() {
        var top = parseInt(this.els.navbar.css('top'), 10);
        var right = parseInt(this.els.navbar.css('right'), 10);
        var bottom = parseInt(this.els.navbar.css('bottom'), 10);
        var left = parseInt(this.els.navbar.css('left'), 10);
        
        var side = '';
        if(right < 0) {
            side = 'right';
        } else if(left < 0) {
            side = 'left';
        } else if(top < 0) {
            side = 'top';
        } else if(bottom < 0) {
            side = 'bottom'
        }
        
        return side;
    },
    /**
     * Hide the Navigation display.
     * @param event     {Event}         Unused.
     * @param timing    {Number}        Override the default time for the animation.
     * @param wait      {Number}        Time to wait before running the animation.
     */
    hideNav: function(event, timing, wait) {
        if(this.timer) { clearTimeout(this.timer); }
   
        var docHeight = this.els.window.height();
        wait = (typeof wait != 'number') ? 1000 : wait;
        timing = (typeof timing != 'number') ? 400 : timing;
        
        this.timer = setTimeout(function() {
            this.els.navtabTextOpen.hide();
            this.els.navtabTextClosed.show();

            var afterAnimate = function() {
                this.navOpen = false;
            }.use(this);
            
            switch(this.navSide) {
                case 'top':
                    this.els.navbar.stop().animate({ top: -this.els.navbar.outerHeight() + 'px' }, timing, 'easeInOutCubic', afterAnimate);
                break;
                case 'bottom':
                    this.els.navbar.stop().animate({ bottom: -this.els.navbar.outerHeight() + 'px' }, timing, 'easeInOutCubic', afterAnimate);
                break;
                case 'right':
                    this.els.navbar.stop().animate({ right: -this.els.navbar.outerHeight() + 'px' }, timing, 'easeInOutCubic', afterAnimate);
                break;
                case 'left':
                    this.els.navbar.stop().animate({ left: -this.els.navbar.outerHeight() + 'px' }, timing, 'easeInOutCubic', afterAnimate);
                break;
                default:
                    this.els.navbar.stop().fadeOut(timing, afterAnimate);
                break;
            }
        }.use(this), wait);
    },
    /**
     * Show the Navigation display.
     * @param event     {Event}         Unused.
     * @param timing    {Number}        Override the default time for the animation.
     */
    showNav: function(event, timing) {
        if(this.currentSlide) {
            this.currentSlideInfo();
        }
        if(this.timer) { clearTimeout(this.timer); }
        
        timing = (typeof timing != 'number') ? 400 : timing;
        
        this.els.navtabTextClosed.hide();
        this.els.navtabTextOpen.show();
 
        var afterAnimate = function() {
            this.navOpen = true;
        }.use(this);
        
        switch(this.navSide) {
            case 'top':
                this.els.navbar.stop().animate({top: '-1px'}, timing, 'easeInOutCubic', afterAnimate);
            break;
            case 'bottom':
                this.els.navbar.stop().animate({bottom: '-1px'}, timing, 'easeInOutCubic', afterAnimate);
            break;
            case 'right':
                this.els.navbar.stop().animate({right: '-1px'}, timing, 'easeInOutCubic', afterAnimate);
            break;
            case 'left':
                this.els.navbar.stop().animate({left: '-1px'}, timing, 'easeInOutCubic', afterAnimate);
            break;
            default:
                this.els.navbar.stop().fadeIn(timing, afterAnimate);
            break;
        }
    },
    /**
     * Toggle the Navigation display.
     * Checks for N keyCode and determines whether to show or hide.
     * @param event     {Event}         Unused.
     */
    toggleNav: function(event) {
        // If key is not 'N', do nothing.
        if(event.keyCode && event.keyCode != 78) { return; }
        if(event) { event.preventDefault(); event.stop(); }
        
        if(this.navOpen) {
            this.hideNav(null, false, 50);
        } else {
            this.showNav();
        }
    },
    /**
     * Start the dragging function on mousedown.
     * @param event     {Event}     For finding the mouse position and determining movement later.
     */
    startDrag: function(event) {
        // if this isn't in the slide content element set, cancel out.
        if(!$(event.target).parents(WebSlide.elements.slideContent).length) { return; }
        
        this.dragging = true;
        this.mouseDragPos = { x: event.screenX, y: event.screenY };
        this.els.document.bind('mousemove.drag', this.drag.useEL(this));
    },
    /**
     * Calculate the movement on mousemove for repositioning the screen during dragging.
     * @param event     {Event}     For finding the mouse position and determining movement.
     */
    drag: function(event) {
        var newX = this.els.document.scrollLeft() + (this.mouseDragPos.x - event.screenX);
        var newY = this.els.document.scrollTop() + (this.mouseDragPos.y - event.screenY);
        
        this.els.document.scrollLeft(newX).scrollTop(newY);
        
        this.mouseDragPos = { x: event.screenX, y: event.screenY };
    },
    /**
     * Kill the dragging function on mouseup.
     * @param event     {Event}     Unused.
     */
    endDrag: function(event) {
        this.dragging = false;
        this.els.document.unbind('mousemove.drag');
    },
    /**
     * Load and show all images and pop up print dialog.
     * @param event     {Event}     For stopping the link action.
     */
    readyPrint: function(event) {
        event.stop();
        
        this.els.document.trigger('print');
  
        this.clearAll();
        this.loadAll(this.showAllAndPrint.use(this));
    },
    /**
     * Clear out all of the current slide HTML so that they will appear in order.
     */
    clearAll: function() {
        this.els.slideContent.text('');
    },
    /**
     * Load all slides.
     * @param callback      {Function}      Return function after all slides are loaded.
     */
    loadAll: function(callback) {
        var callback = (callback) ? callback : function() {};
        var i = this.slides.length;
        while(i--) {
            if(i == 0) {
                this.preload(i, callback);
            } else {
                this.preload(i);
            }
        }
    },
    /**
     * Show all slides and print the page. Set to last slide.
     */
    showAllAndPrint: function() {
        var i = this.slides.length;
        var j = i-1;
        while(i--) {
            this.appendSlide(this.slides[i]);
        }
        
        // Set the current slide to the last slide
        this.advance(null, j);
        
        // Adding large images to the DOM continues running as JavaScript is still running.
        // Because of that, we have to wait about a second.
        setTimeout(function() {
            $(this.slides).each(function() { this.show(0); });
            window.print();
        }.use(this), 1000);
    },
    /**
     * Toggle the help dialog.
     * Checks for H keyCode and determines whether to show or hide.
     * @param event     {Event}     Used for keyboard detection.
     */
    toggleHelpDialog: function(event) {
        if(event) {
            event.stop();
            if(event.keyCode && event.keyCode != 72) {
                return;
            }
        }
        
        if(!this.els.helpDialog.hasClass('visible')) {
            this.showHelpDialog();
        } else {
            this.hideHelpDialog();
        }
    },
    /**
     * Show the help dialog.
     * Centers the help dialog in the window and fades in.
     * @param event     {Event}     Unused.
     */
    showHelpDialog: function(event) {
        if(event) {
            event.stop();
        }
        
        var windowWidth = this.els.window.width();
        var windowHeight = this.els.window.height();
        
        var maxWidth = windowWidth - Math.round(windowWidth / 2);
        this.els.helpDialog.width(maxWidth);
        
        var maxHeight = windowHeight - Math.round(this.els.navbar.outerHeight() * 3);
        if(this.els.helpDialog.height() > maxHeight) {
            this.els.helpDialog.height(maxHeight);
        }
        
        var leftPos = this.els.document.scrollLeft() + ((windowWidth / 2) - (this.els.helpDialog.width() / 2));
        var topPos = this.els.document.scrollTop() + ((this.els.window.height() / 2) - (this.els.helpDialog.height() / 2));
        
        this.els.helpDialog.hide().css({ left: leftPos+'px', top: topPos+'px' }).fadeIn(600).addClass('visible');
        
        this.els.document.bind('slidechange zoom print', this.hideHelpDialog.useEL(this));
    },
    /**
     * Hide the help dialog.
     * @param event     {Event}     Unused.
     */
    hideHelpDialog: function(event, data) {
        if(event) {
            event.stop();
        }
        
        this.els.helpDialog.fadeOut(500).removeClass('visible');
    }
});

/**
 * Slide class that every slide/image instance is bound to.
 * @param data      {Object}        Object from the array of slides set in the user config.
 */
var Slide = function(data) {
    this.file = data.file;
    this.title = data.title;
    this.description = data.description;
    this.cache, this.src;
    this.els = {
        document: $(document),
        window: $(window)
    };
};
$.extend(Slide.prototype, {
    /**
     * Preload the image for the slide.
     * @param callback      {Function}      Optional function to run on image load success.
     * @param error         {Function}      Optional function to run on image load error.
     */
    preload: function(callback, error) {
        callback = (callback) ? callback : function() {};
        error = (error) ? error : function() {};
        
        // Check if the image is already loaded/cached.
        if(!this.cache) {
            this.cache = new Image();
            
            // Globally announce that we're loading something.
            this.els.document.trigger('loadingStart');
            
            this.cache.onload = function() {
                // Hold the dimensions in an array for referencing later during zooms.
                this.dimensions = {
                    height: this.cache.height,
                    width: this.cache.width
                };
                
                // Success! Fire the callback.
                callback();
                
                // Globally announce that loading finished.
                this.els.document.trigger('loadingComplete');
            }.use(this);
            
            this.cache.onerror = function() {
                // Globally announce that loading errored.
                this.els.document.trigger('loadingError');
                
                // Nullify the cache since it couldn't be loaded.
                this.cache = null;
                
                // Run the error callback.
                error();
            }.use(this);
            
            // create a new cache instance in this Slide object.
            this.cache.src = this.src;
            $(this.cache).hide();
        } else {
            callback();
        }
    },
    /**
     * Place the slide onto our document in the correct position.
     * @param position      {String}        Space-separated string of horizontal/vertical alignment.
     * @param context       {Array}         [Width,Height] of the context in which we are placing this slide.
     */        
    setPosition: function(position, context) {
        if(!this.dimensions) { return; }
        this.position = position || this.position;
        var dimensions = this.zoomDimensions || this.dimensions;
        
        var leftPos, topPos;
        switch(this.position[0].toLowerCase()) {
            case 'left':
                leftPos = '0';
                break;
            case 'right':
                leftPos = Math.abs(context[0]-dimensions.width);
                break;
            default:
                leftPos = Math.abs(Math.round(context[0]/2)-Math.round(dimensions.width/2));
                break;
        }
        
        switch(this.position[1].toLowerCase()) {
            case 'bottom':
                topPos = Math.abs(context[1]-dimensions.height);
                break;
            case 'center':
                topPos = Math.abs(Math.round(context[1]/2)-Math.round(dimensions.height/2));
                break;
            default:
                topPos = '0';
                break;
        }
        
        if(dimensions.height <= context[1] || dimensions.width <= context[0]) {
            if(dimensions.height <= context[1]) {
                $(this.cache).css({top: topPos+'px'});
                $('body').css({height:context[1]+'px'});
            } else {
                $('body').css({height:dimensions.height+'px'});
                this.els.document.scrollTop(topPos);
            }
            if(dimensions.width <= context[0]) {
                $(this.cache).css({left: leftPos+'px'});
                $('body').css({width:context[0]+'px'});
            } else {
                $('body').css({width:dimensions.width+'px'});
                this.els.document.scrollLeft(leftPos);
            }
        } else {
            $('body').css({width:dimensions.width+'px', height:dimensions.height+'px'});
            this.els.document.scrollTop(topPos);
            this.els.document.scrollLeft(leftPos);
        }

        this.zoomDimensions = false;
    },
    /**
     * Animate the opacity out to hide the slide image.
     */
    hide: function(timing) {
        var timing = (timing != 'undefined') ? timing : 500;
        this.els.document.unbind('keyup', this.zoomFn);
        
        if(timing == 0) {
            $(this.cache).hide();
        } else {
            $(this.cache).fadeOut(timing);
        }
    },
    /**
     * Animate the opacity in to show the slide image.
     */
    show: function(timing) {
        var timing = (timing != 'undefined') ? timing : 500;
        if(this.dimensions) {
            $(this.cache).css({ width: this.dimensions.width+'px', height: this.dimensions.height+'px' });
        }
        
        this.zoomFn = this.zoom.use(this);
        this.els.document.bind('keyup', this.zoomFn);
        if(timing == 0) {
            $(this.cache).show();
        } else {
            $(this.cache).fadeIn(timing);
        }
    },
    /**
     * Zoomify the image in or out.
     * @param event         {Event}         Keycode event to determine zooming in or out.
     */
    zoom: function(event) {
        var finish = function() {
            this.zoomDimensions = {
                height: this.cache.offsetHeight,
                width: this.cache.offsetWidth
            };
            this.setPosition(null, [this.els.window.width(), this.els.window.height()]);
        };
        if(event.keyCode == 90 || event.keyCode == 88) {
            this.els.document.trigger('zoom');

            // Set the percent difference to scale the image to from its current size.
            var change = 80;
            if(event.keyCode == 90) { change = 120; } 
            
            var horizontalPos = this.position[0];
            var verticalPos = (this.position[1] == 'center') ? 'middle' : this.position[1];
            
            $(this.cache).effect('scale', { percent: change, origin: [verticalPos, horizontalPos] }, 400, finish.use(this));
        }
    }
});

$(document).ready(WebSlide.init);
