
var Sgss = Sgss || {};

(function() {

var baseUrl;
var documentTitle;
var cssTransitionDuration = 0.1;

(function() {
    var loc = window.location;
    baseUrl = loc.protocol + '//' + loc.hostname
    if (loc.pathname.endsWith('/')) {
        var components = loc.pathname.split('/');
        components.pop();
        baseUrl += components.join('/') + '/';
    } else {
        baseUrl += loc.pathname;
    }
    documentTitle = document.title;
})();

function divc() {
    var result = '';
    $A(arguments).each(function(className) {
        result += '<div class="' + className + '"></div>';
    });
    return result;
}

function spanc() {
    var result = '';
    $A(arguments).each(function(className) {
        result += '<span class="' + className + '"></span>';
    });
    return result;
}

function filename(url) {
    return url.split('/').last().split('.')[0];
}

function swapExtension(url, extension) {
    return url.gsub(/\.[a-z0-9]+$/, '.' + extension);
}

function writeCookie(name, value, expirationDay) {
    var expiration = '';
    if (expirationDay) {
        var date = new Date();
        date.setTime(date.getTime() + expirationDay * 24 * 60 * 60 * 1000);
        expiration = '; expires=' + date.toGMTString();
    }
    document.cookie = name + '=' + escape(String(value)) + '; path=/' + expiration;
}

function readCookie(name) {
    var result = null;
    if(document.cookie.length > 0) {
        var cookieName = name + '=';
        var start = document.cookie.indexOf(cookieName);
        if (start > -1) {
            start += cookieName.length;
            var end = document.cookie.indexOf(';', start);
            if(end < startIndex) {
                end = document.cookie.length; 
            }
            result = unescape(document.cookie.substring(start, end));
        }
    }
    return result;
}

/* ---------- Root ---------------------------------------------------------- */

Sgss.Root = Class.create((function() {
    var minWidth = 990;
    var maxWidth = 1264;
    
    return {
        initialize: function(element, options) {
            this.element = $(element);
            this.options = options || {};
            this.widthDeduction = parseInt($(document.body).getStyle('margin-left'))
                                + parseInt($(document.body).getStyle('margin-right'));
            
            this.updateDimensions();
            Event.observe(window, 'resize', this.updateDimensions.bindAsEventListener(this));
            
            // Restore what I did in the preflight script just after the 
            // interpreter becomes idle (calling defer)
            (function() {
                this.element.setStyle({ visibility: 'visible' });
            }).bind(this).defer();
        },
        
        updateDimensions: function(e) {
            // I want the scrollbar to show and hide without affecting the 
            // dimensions of the root element. This can be done simply by 
            // substracting the width of the root by that of the scrollbar
            // (0 when it is not shown).
            
            $(document.body).setStyle({ overflow: 'hidden' });
            var viewportWidth = document.viewport.getWidth();
            $(document.body).setStyle({ overflow: 'auto' });
            var width = (viewportWidth > maxWidth ? maxWidth :
                        (viewportWidth < minWidth ? minWidth : viewportWidth));
            this.element.setStyle({ width: width - this.widthDeduction + 'px' });
        }
    };
})());

/* ---------- Showcase ------------------------------------------------------ */

Sgss.Showcase = Class.create((function() {
    var animationDuration = 0.3;
    var aspectRatio = 0.5625;
    
    // Returns a object that represents name-value options specified in the 
    // given type expression in format like "type(name=value, ....)"
    function extractTypeOptions(type) {
        var options = {};
        if (type.match(/^\w+\(([^)]+)\)$/)) {
            RegExp.lastParen.split(/,\s*/).each(function(pair) {
                pair = pair.split(/\s*=\s*/);
                options[pair[0]] = pair[1];
            });
        }
        return options;
    }
    
    var result = {
        initialize: function(container, options) {
            this.container = $(container);
            this.element = container.down('div.showcase');
            this.options = options || {};
            this.navigationElement = container.down('.navigation');
            this.captionElement = container.down('.caption');
            
            // Retrieve content data from all the list items in the navigation 
            // element
            this.contents = this.navigationElement.select('li').collect(function(listItem, index) {
                var anchorElement = listItem.down('a');
                var url           = anchorElement.readAttribute('href');
                var title         = anchorElement.innerHTML;
                var hashName      = filename(url);
                var hash          = '#' + hashName;
                
                if (this.options.needsChangeHash) {
                    anchorElement.writeAttribute('href', hash);
                } else {
                    anchorElement.writeAttribute('href', null);
                    Event.observe(anchorElement, 'click', this.loadContentWithIndex.bind(this, index, true));
                }
                anchorElement.writeAttribute('title', title.escapeHTML());
                
                // Extract a type and options from the rel attribute of the list
                // item
                var relAttribute = anchorElement.readAttribute('rel');
                relAttribute.match(/^(\w+)/);
                var type = RegExp.lastParen;
                var options = extractTypeOptions(relAttribute);
                anchorElement.writeAttribute('rel', null);
                
                return {
                    element       : null,
                    delegate      : null, 
                    anchorElement : anchorElement,
                    title         : title,
                    hashName      : hashName,
                    hash          : hash,
                    url           : url,
                    type          : type,
                    options       : options
                };
            }, this);
            this.visibleContents = [];
            this.selectedContent = null;
            this.selectedContentIndex = null;
            
            if (this.contents.length == 1) {
                // Everyone don't need navigation when only one content is there
                this.navigationElement.setStyle({ visibility: 'hidden' });
            
            } else {
                if (this.options.showNavigation) {
                    // Arrows at the both side of the group of anchors
                    this.nextNavigationElement = new Element('a').update('Previous');
                    this.previousNavigationElement = new Element('a').update('Next');
                    this.contents.last().anchorElement.up().insert({ after: new Element('li', { 'class': 'next' }).insert(this.nextNavigationElement) });
                    this.contents.first().anchorElement.up().insert({ before: new Element('li', { 'class': 'previous' }).insert(this.previousNavigationElement) });
                } else {
                    this.navigationElement.setStyle({ visibility: 'hidden' });
                }
                
                // Overlay navigation over contents
                if (this.options.useOverlayNavigation) {
                    this.element.insert(this.nextOverlayElement = new Element('a', { 'class': 'overlay-next' }));
                    this.element.insert(this.previousOverlayElement = new Element('a', { 'class': 'overlay-previous' }));
                    
                    // Highlight navigation arrows when the overlays receive 
                    // rollover events
                    Event.observe(this.nextOverlayElement, 'mouseover', this.nextNavigationElement.addClassName.bind(this.nextNavigationElement, 'focused'));
                    Event.observe(this.nextOverlayElement, 'mouseout', this.nextNavigationElement.removeClassName.bind(this.nextNavigationElement, 'focused'));
                    Event.observe(this.previousOverlayElement, 'mouseover', this.previousNavigationElement.addClassName.bind(this.previousNavigationElement, 'focused'));
                    Event.observe(this.previousOverlayElement, 'mouseout', this.previousNavigationElement.removeClassName.bind(this.previousNavigationElement, 'focused'));
                }
            }
            
            // Placeholder for the caption of visible content. Stored opacity
            // values of them are used to restore opacity in fading animation.
            if (this.captionElement) {
                if (this.nameCaptionElement = this.captionElement.down('.name.individual')) {
                    this.nameCaptionElement.insert(spanc('fontaine-alt'));
                    this.nameCaptionOpacity = parseFloat(this.nameCaptionElement.getStyle('opacity'));
                }
                if (this.dateCaptionElement = this.captionElement.down('.date.individual')) {
                    this.dateCaptionElement.insert(spanc('fontaine-alt'));
                    this.dateCaptionOpacity = parseFloat(this.dateCaptionElement.getStyle('opacity'));
                }
            }
            
            // Preload caption images because I don't want the caption images to 
            // be loaded while animating them
            this.contents.each(function(content) {
                if (this.nameCaptionElement) {
                    new Image().src = 'img/title_' + content.hashName + '.png';
                }
                if (this.dateCaptionElement) {
                    new Image().src = 'img/date_' + content.hashName + '.png';
                }
            }, this);
            
            
            // Load the initial content
            var contentIndex = this.contentIndexForHash(window.location.hash) || 0;
            this.loadContentWithIndex(contentIndex, false);
            this.preloadContents();
            
            // Observe location.hash for its changes to reload content
            if ('onhashchange' in window) {
                Event.observe(window, 'hashchange', (function(e) {
                    this.loadContentWithIndex(this.contentIndexForHash(window.location.hash) || 0, true);
                }).bindAsEventListener(this));
            
            } else {
                var storedHash = window.location.hash;
                this.__hashObservingTimer = setInterval((function() {
                    if (window.location.hash != storedHash) {
                        storedHash = window.location.hash;
                        this.loadContentWithIndex(this.contentIndexForHash(window.location.hash) || 0, true);
                    }
                }).bind(this), 100);
            }
            
            // Wrap the showcase in a frame
            new Sgss.Showcase.Frame(this.element);
            
            this.updateDimensions();
            Event.observe(window, 'resize', this.updateDimensions.bindAsEventListener(this));
        },
        
        contentIndexForHash: function(hash) {
            var index = null;
            this.contents.each(function(content, i) {
                if (content.hash == hash) {
                    index = i;
                    throw $break;
                }
            });
            return index;
        },
        
        updateDimensions: function(e) {
            var bodyWidth = $('body').getWidth();
            var contentWidth = bodyWidth;
            var contentHeight = Math.round(bodyWidth * (this.options.aspectRatio || aspectRatio));
            this.element.setStyle({
                width  : contentWidth + 'px',
                height : contentHeight + 'px'
            });
            if (this.navigationElement) {
                this.navigationElement.setStyle({ width: contentWidth + 'px' });
            }
            if (this.videoControllerElement) {
                this.videoControllerElement.setStyle({ width: contentWidth + 'px' });
            }
            
            // Synchronously update the dimensions of the contents that is 
            // currently visible
            this.visibleContents.each(function(content) {
                content.delegate.updateDimensions();
            }, this);
        }
    };
    
    /* ---------- Manipulation -----------------------------------------------*/
    
    function paragraphForContent(content) {
        var id = 'p_' + content.hashName.gsub(/\d+$/, '');
        return $(id) || null;
    }
    
    Object.extend(result, {
        _numContentLocks: 0,
        _pendingContent: null,
        
        addContent: function(content) {
            if (content) {
                this.element.insert(content.element);
                this.visibleContents.push(content);
            }
        },
        
        removeContent: function(content) {
            if (content) {
                this.visibleContents.splice(this.visibleContents.indexOf(content), 1);
                content.element.remove();
            }
        },
        
        canLoadContent: function() {
            return this._numContentLocks == 0;
        },
        
        lockContent: function() {
            this._numContentLocks ++;
        },
        
        releaseContent: function() {
            this._numContentLocks --;
            if (this.canLoadContent()) {
                // Load any content that is pending because it was called while 
                // locked
                if (this._pendingContent) {
                    this.loadContentWithIndex(this._pendingContent.index, this._pendingContent.animated);
                    this._pendingContent = null;
                }
            }
        },
        
        loadContentWithIndex: function(index, animated) {
            if (this.canLoadContent()) {
                this.lockContent();
                if (index != this.selectedContentIndex && this.contents[index]) {
                    this.stopAutopilot();
                    
                    var newContent, oldContent;
                    
                    // Make a new content
                    newContent = this.contents[index];
                    if (!newContent.delegate) {
                        if (newContent.type == 'video' && !this.videoController) {
                            // Prepare video controller before creating delegate 
                            this.prepareVideoController();
                        }
                        newContent.delegate = Sgss.Showcase.Delegate.create(this, newContent);
                        newContent.element = new Element('div', { 'class': 'content' });
                        newContent.element.insert(newContent.delegate.element);
                    }
                    
                    // Mark the anchor as selected
                    newContent.anchorElement.addClassName('selected');
                    newContent.anchorElement.setStyle({ cursor: 'default' });
                    
                    if (this.selectedContentIndex != null) {
                        // We have an old content being visible now
                        oldContent = this.contents[this.selectedContentIndex];
                        
                        // Mark the anchor as unselected
                        oldContent.anchorElement.removeClassName('selected');
                        oldContent.anchorElement.setStyle({ cursor: 'pointer' });
                        
                        if (!animated) {
                            // Immediately remove the old content, otherwise it 
                            // will be removed after the loading effect finishes
                            this.removeContent(oldContent);
                            if (oldContent.delegate.beforeBecomeHidden) {
                                oldContent.delegate.beforeBecomeHidden(false);
                            }
                            if (oldContent.delegate.afterBecomeHidden) {
                                oldContent.delegate.afterBecomeHidden(false);
                            }
                        }
                    }
                    if (animated) {
                        newContent.element.setOpacity(0);
                        new Effect.Opacity(newContent.element, {
                            to          : 1.0, 
                            duration    : (this.options.animationDuration || animationDuration),
                            beforeStart : (function(newContent, oldContent) {
                                this.lockContent();
                                if (newContent.delegate.beforeBecomeVisible) {
                                    newContent.delegate.beforeBecomeVisible(true);
                                }
                                if (oldContent.delegate.beforeBecomeHidden) {
                                    oldContent.delegate.beforeBecomeHidden(true);
                                }
                            }).bind(this, newContent, oldContent),
                            afterFinish : (function(newContent, oldContent) {
                                if (newContent.delegate.afterBecomeVisible) {
                                    newContent.delegate.afterBecomeVisible(true);
                                }
                                if (oldContent) {
                                    // Now animation finished, remove the old content
                                    this.removeContent(oldContent);
                                    if (oldContent.delegate.afterBecomeHidden) {
                                        oldContent.delegate.afterBecomeHidden(true);
                                    }
                                }
                                this.releaseContent();
                            }).bind(this, newContent, oldContent)
                        });
                    }
                    
                    this.addContent(newContent);
                    this.selectedContent = newContent;
                    this.selectedContentIndex = index;
                    if (!animated) {
                        if (newContent.delegate.beforeBecomeVisible) {
                            newContent.delegate.beforeBecomeVisible(false);
                        }
                        if (newContent.delegate.afterBecomeVisible) {
                            newContent.delegate.afterBecomeVisible(false);
                        }
                    }
                    
                    this.updateDimensions();
                    this.updateCaptionForContent(newContent, animated);
                    this.updateNavigationForContent(newContent, animated);
                    
                    // Highlight a corresponding paragraph if any
                    var paragraph;
                    if (oldContent && (paragraph = paragraphForContent(oldContent))) {
                        paragraph.removeClassName('highlighted');
                    }
                    if (paragraph = paragraphForContent(newContent)) {
                        paragraph.addClassName('highlighted');
                    }
                    
                    // Update the title bar
                    if (this.options.needsChangeTitle) {
                        (function() {
                            if (window.location.hash == newContent.hash) {
                                document.title = documentTitle + ' | ' + newContent.title.escapeHTML();
                            } else {
                                document.title = documentTitle;
                            }
                        }).defer();
                    }
                    
                    if (this.options.enableAutopilot) {
                        this.resetAutopilot(newContent.delegate.interruptTransition);
                    }
                }
                this.releaseContent();
            
            } else {
                // Store load request called while locked. Any previous request will
                // be overriden.
                this._pendingContent = {
                    index:    index,
                    animated: animated
                };
            }
        },
        
        loadNextContent: function(animated, cyclic) {
            if (this.selectedContentIndex < this.contents.length - 1) {
                this.loadContentWithIndex(this.selectedContentIndex + 1, animated);
            } else if (cyclic) {
                this.loadContentWithIndex(0, animated);
            }
        }, 
        
        loadPreviousContent: function(animated, cyclic) {
            if (this.selectedContentIndex > 0) {
                this.loadContentWithIndex(this.selectedContentIndex - 1, animated);
            } else if (cyclic) {
                this.loadContentWithIndex(this.contents.length - 1, animated);
            }
        }
    });
    
    /* ---------- Preloading ----------*/
    
    Object.extend(result, {
        _contentPreloadStarted: [],
        
        preloadContents: function() {
            var preloadIndex = -1;
            
            // Look forward for unloaded contents 
            for (var i = this.selectedContentIndex; i < this.contents.length; i ++) {
                if (!this._contentPreloadStarted[i]) {
                    preloadIndex = i;
                    break;
                }
            }
            // Look backward for unloaded contents 
            if (preloadIndex < 0) {
                for (var i = this.selectedContentIndex; i >= 0; i --) {
                    if (!this._contentPreloadStarted[i]) {
                        preloadIndex = i;
                        break;
                    }
                }
            }
            // We found an unloaded content
            if (preloadIndex >= 0) {
                this._contentPreloadStarted[preloadIndex] = true;
                var content = this.contents[preloadIndex];
                if (content.type == 'image' || content.type == 'digest' || content.type == 'video') {
                    var imageUrl = content.url;
                    if (content.type == 'video') {
                        imageUrl = content.options.poster;
                    }
                    if (imageUrl) {
                        var image = new Image();
                        // Continue recursively when the image has finished loading
                        var listener = (function(e) {
                            Event.stopObserving(image, 'load', listener);
                            this.preloadContents();
                        }).bindAsEventListener(this);
                        Event.observe(image, 'load', listener);
                        image.src = swapExtension(imageUrl, 'jpg');
                    
                    } else {
                        // We found a potential preloadable content, but the url
                        // was not found, proceed to the next content
                        this.preloadContents();
                    }
                }
            }
        }
    });
    
    /* ---------- Navigation ----------*/
    
    Object.extend(result, {
        updateCaptionForContent: function(content, animated) {
            if (this.captionElement) {
                if (animated) {
                    // Make subeffects of the fade-out animation for the old
                    // caption
                    var subeffects = [];
                    if (this.nameCaptionElement) {
                        subeffects.push(new Effect.Opacity(this.nameCaptionElement, { sync: true, to: 0.0 }));
                    }
                    if (this.dateCaptionElement) {
                        subeffects.push(new Effect.Opacity(this.dateCaptionElement, { sync: true, to: 0.0 }));
                    }
                    new Effect.Parallel(subeffects, {
                        duration    : (this.options.animationDuration || animationDuration) / 2,
                        beforeStart : this.lockContent.bind(this),
                        afterFinish : (function() {
                            // Change the images this time
                            this.updateCaptionImagesForContent(content);
                            
                            // Make subeffects of the fade-in animation for the
                            // new caption
                            var subeffects = [];
                            if (this.nameCaptionElement) {
                                subeffects.push(new Effect.Opacity(this.nameCaptionElement, { sync: true, to: this.nameCaptionOpacity }));
                            }
                            if (this.dateCaptionElement) {
                                subeffects.push(new Effect.Opacity(this.dateCaptionElement, { sync: true, to: this.dateCaptionOpacity }));
                            }
                            new Effect.Parallel(subeffects, {
                                duration    : (this.options.animationDuration || animationDuration) / 2,
                                afterFinish : this.releaseContent.bind(this)
                            });
                        }).bind(this)
                    });
                    
                } else {
                    this.updateCaptionImagesForContent(content);
                }
            }
        }, 
        
        updateCaptionImagesForContent: function(content) {
            if (this.nameCaptionElement) {
                this.nameCaptionElement.down('.fontaine-alt').setStyle({
                    backgroundImage: 'url("img/title_' + content.hashName + '.png")'
                });
            }
            if (this.dateCaptionElement) {
                this.dateCaptionElement.down('.fontaine-alt').setStyle({
                    backgroundImage: 'url("img/date_' + content.hashName + '.png")'
                });
            }
        }, 
        
        updateNavigationForContent: function(content, animated) {
            // Previous navigation
            if (this.previousNavigationElement) {
                if (this.selectedContentIndex > 0) {
                    // Change the attribute of the anchor
                    (function() {
                        var content = this.contents[this.selectedContentIndex - 1];
                        if (this.options.needsChangeHash) {
                            this.previousNavigationElement.writeAttribute('href', content.hash);
                        } else {
                            this.previousNavigationElement.writeAttribute('href', null);
                            this.previousNavigationElement.onclick = (function() { this.loadPreviousContent(true, false) }).bind(this);
                        }
                        this.previousNavigationElement.writeAttribute('title', 'Previous: ' + content.title.escapeHTML());
                    }).bind(this).defer();
                    
                    this.showContextualNavigationElement(this.previousNavigationElement, animated);
                } else {
                    this.hideContextualNavigationElement(this.previousNavigationElement, animated);
                }
            }
            
            // Next navigation
            if (this.nextNavigationElement) {
                if (this.selectedContentIndex < this.contents.length - 1) {
                    // Change the attribute of the anchor
                    (function() {
                        var content = this.contents[this.selectedContentIndex + 1];
                        if (this.options.needsChangeHash) {
                            this.nextNavigationElement.writeAttribute('href', content.hash);
                        } else {
                            this.nextNavigationElement.writeAttribute('href', null);
                            this.nextNavigationElement.onclick = (function() { this.loadNextContent(true, false) }).bind(this);
                        }
                        this.nextNavigationElement.writeAttribute('title', 'Next: ' + content.title.escapeHTML());
                    }).bind(this).defer();
                    
                    this.showContextualNavigationElement(this.nextNavigationElement, animated);
                } else {
                    this.hideContextualNavigationElement(this.nextNavigationElement, animated);
                }
            }
            
            // Overlay anchors, which are invisible layers of anchor element 
            // over the content. We must change the href of them everytime the 
            // content changes here.
            if (this.previousOverlayElement) {
                (function() {
                    if (this.selectedContentIndex > 0) {
                        var content = this.contents[this.selectedContentIndex - 1];
                        if (this.options.needsChangeHash) {
                            this.previousOverlayElement.writeAttribute('href', content.hash);
                        } else {
                            this.previousOverlayElement.writeAttribute('href', null);
                            this.previousOverlayElement.onclick = (function() { this.loadPreviousContent(true, false) }).bind(this);
                        }
                    } else {
                        this.previousOverlayElement.writeAttribute('href', null);
                    }
                }).bind(this).defer();
            }
            if (this.nextOverlayElement) {
                (function() {
                    if (this.selectedContentIndex < this.contents.length - 1) {
                        var content = this.contents[this.selectedContentIndex + 1];
                        if (this.options.needsChangeHash) {
                            this.nextOverlayElement.writeAttribute('href', content.hash);
                        } else {
                            this.nextOverlayElement.writeAttribute('href', null);
                            this.nextOverlayElement.onclick = (function() { this.loadNextContent(true, false) }).bind(this);
                        }
                    } else {
                        this.nextOverlayElement.writeAttribute('href', null);
                    }
                }).bind(this).defer();
            }
        }, 
        
        showContextualNavigationElement: function(anchorElement, animated) {
            if (anchorElement.up().getStyle('visibility') == 'hidden') {
                anchorElement.up().setStyle({ visibility: 'visible' });
                if (animated) {
                    new Effect.Opacity(anchorElement.up(), {
                        to          : 1.0,
                        duration    : (this.options.animationDuration || animationDuration) / 2,
                        beforeStart : this.lockContent.bind(this),
                        afterFinish : this.releaseContent.bind(this)
                    });
                    
                } else {
                    anchorElement.up().setOpacity(1);
                }
            }
        }, 
        
        hideContextualNavigationElement: function(anchorElement, animated) {
            if (animated) {
                new Effect.Opacity(anchorElement.up(), {
                    to          : 0.0,
                    duration    : (this.options.animationDuration || animationDuration) / 2,
                    beforeStart : this.lockContent.bind(this),
                    afterFinish : (function() {
                        anchorElement.writeAttribute('href', null);
                        anchorElement.up().setStyle({ visibility: 'hidden' });
                        this.releaseContent();
                    }).bind(this)
                });
                
            } else {
                anchorElement.up().setOpacity(0);
                anchorElement.up().setStyle({ visibility: 'hidden' });
            }
        }
    });
    
    /* ---------- Autopilot ----------*/
    
    var transitionInterval = 5;
    var progressResolution = 36;
    var postponedProgressValue = 28;
    
    function progressIntervalTimerFired() {
        var progressElement = this.navigationElement.select('li a.selected').first();
        
        if (this._progressValue < progressResolution) {
            // Animate the progress element
            progressElement.setStyle({ backgroundPosition: '-' + progressElement.getWidth() * this._progressValue + 'px center' });
            
            if (this._progressValue == 0) {
                // Switch the style of the button to progress indicator
                progressElement.addClassName('progress');
            }
            // Advance the progress value
            if (this._progressValue < postponedProgressValue) {
                this._progressValue ++;
            } else if (!this._shouldPostponeTransition) {
                this._progressValue ++;
            }
            
        } else {
            // Revert to the initial style
            progressElement.removeClassName('progress');
            progressElement.setStyle({ backgroundPosition: 'center center' });
            
            // Advance to the next content
            this.loadNextContent(true, true);
        }
    }
    
    function holdTransition() {
        this._shouldPostponeTransition = true;
        if (this._progressValue > postponedProgressValue) {
            this._progressValue = postponedProgressValue;
        }
    }
    
    function releaseTransitionHold() {
        this._shouldPostponeTransition = false;
    }
    
    Object.extend(result, {
        resetAutopilot: function(interruptTransition) {
            // Make an interval timer to advance the progress value
            this._progressValue = 0;
            this.__progressIntervalTimer = setInterval(progressIntervalTimerFired.bind(this), ((this.options.transitionInterval || transitionInterval) * 1000) / progressResolution);
            
            if (interruptTransition) {
                // Observe for the user interacts with the showcase to postpone 
                // transition
                if (this.__transitionHoldListener) {
                    Event.stopObserving(this.element, 'mouseover', this.__transitionHoldListener);
                }
                this.__transitionHoldListener = holdTransition.bind(this);
                Event.observe(this.element, 'mouseover', this.__transitionHoldListener);
                
                // Observe for the user stopped interacting with the showcase to 
                // be ready to do transition
                if (this.__transitionReleaseListener) {
                    Event.stopObserving(this.element, 'mouseout', this.__transitionReleaseListener);
                }
                this.__transitionReleaseListener = releaseTransitionHold.bind(this);
                Event.observe(this.element, 'mouseout', this.__transitionReleaseListener);
                
            } else {
                this._shouldPostponeTransition = false;
            }
        },
        
        stopAutopilot: function() {
            // Invalidate the interval timer and event observations
            if (this.__progressIntervalTimer) {
                clearInterval(this.__progressIntervalTimer);
                this.__progressIntervalTimer = null;
            }
            if (this.__transitionHoldListener) {
                Event.stopObserving(this.element, 'mouseover', this.__transitionHoldListener);
                this.__transitionHoldListener = null;
            }
            if (this.__transitionReleaseListener) {
                Event.stopObserving(this.element, 'mouseout', this.__transitionReleaseListener);
                this.__transitionReleaseListener = null;
            }
            this._progressValue = 0;
            
            // Revert to the initial style
            var progressElement = this.navigationElement.select('li a.selected').first();
            if (progressElement) {
                progressElement.removeClassName('progress');
                progressElement.setStyle({ backgroundPosition: 'center center' });
            }
        }
    });
    
    /* ---------- Video Control ----------*/
    
    var controllerSwapLength = 15;
    
    Object.extend(result, {
        _videoControllerVisible: false,
        
        prepareVideoController: function() {
            this.videoController = new Sgss.Showcase.VideoController(this);
            this.videoControllerElement = this.videoController.element;
            this.videoControllerElement.hide();
            this.navigationElement.insert({ after: this.videoControllerElement });
        },
        
        showVideoController: function(animated) {
            if (!this._videoControllerVisible) {
                if (animated) {
                    this.videoControllerElement.setStyle({
                        marginTop : - controllerSwapLength + 'px',
                        opacity   : 0
                    });
                    // Make subeffects of the both controller element and navigation
                    // element
                    var subeffects = [
                        new Effect.Morph(this.navigationElement, { sync: true, style: 'margin-top: ' + controllerSwapLength + 'px;' }),
                        new Effect.Opacity(this.navigationElement, { sync: true, to: 0 }),
                        new Effect.Morph(this.videoControllerElement, { sync: true, style: 'margin-top: 0;' }),
                        new Effect.Opacity(this.videoControllerElement, { sync: true, to: 1 }),
                    ];
                    this.videoControllerElement.show();
                    new Effect.Parallel(subeffects, {
                        duration    : (this.options.animationDuration || animationDuration),
                        beforeStart : this.lockContent.bind(this),
                        afterFinish : (function() {
                            this.navigationElement.hide();
                            this.releaseContent();
                        }).bind(this)
                    });
                
                } else {
                    this.navigationElement.hide();
                    this.videoControllerElement.show();
                }
                this._videoControllerVisible = true;
            }
        }, 
        
        hideVideoController: function(animated) {
            if (this._videoControllerVisible) {
                if (animated) {
                    this.navigationElement.setStyle({
                        marginTop : controllerSwapLength + 'px',
                        opacity   : 0
                    });
                    // Make subeffects of the both controller element and navigation
                    // element
                    var subeffects = [
                        new Effect.Morph(this.navigationElement, { sync: true, style: 'margin-top: 0;' }),
                        new Effect.Opacity(this.navigationElement, { sync: true, to: 1 }),
                        new Effect.Morph(this.videoControllerElement, { sync: true, style: 'margin-top: -' + controllerSwapLength + 'px;' }),
                        new Effect.Opacity(this.videoControllerElement, { sync: true, to: 0 }),
                    ];
                    
                    this.navigationElement.show();
                    new Effect.Parallel(subeffects, {
                        duration    : (this.options.animationDuration || animationDuration),
                        beforeStart : this.lockContent.bind(this),
                        afterFinish : (function() {
                            this.videoControllerElement.hide();
                            this.releaseContent();
                        }).bind(this)
                    });
                
                } else {
                    this.navigationElement.show();
                    this.videoControllerElement.hide();
                }
                this._videoControllerVisible = false;
            }
        }
    });
    
    return result;
})());

/* ---------- Video Controller ---------------------------------------------- */
// This class handles *only* ui events and the resulting controls to the video
// element. Any event invoked from the video element is caught by the video 
// delegate and passed to this controller.

Sgss.Showcase.VideoController = Class.create((function() {
    var volumeSliderWidth = 60;
    var volumeSliderOffset = 24;
    var seekBarWidth = 300;
    var seekBarOffset = 6;
    
    function defocusVolumeBox() {
        this.volumeBox.removeClassName('focused');
        this.volumeButton.removeClassName('focused');
        // Hide the volume box after its css animation finishes
        setTimeout((function() {
            this.volumeBox.hide();
        }).bind(this), cssTransitionDuration * 1000);
    }
    
    return {
        initialize: function(showcase) {
            this.showcase = showcase;
            this.element = new Element('div', { 'class': 'controller' });
            
            this.closeButton = new Element('a', { 'class': 'close', 'title': 'Close' });
            this.playPauseButton = new Element('a', { 'class': 'play', 'title': 'Play' });
            this.volumeButton = new Element('a', { 'class': 'volume', 'title': 'Volume' });
            this.seekBar = new Element('div', { 'class': 'seekbar' });
            this.seekActive = new Element('div', { 'class': 'seek-active' });
            this.seekInactive = new Element('div', { 'class': 'seek-inactive' });
            this.seekThumb = new Element('div', { 'class': 'thumb' });
            this.volumeBox = new Element('div', { 'class': 'volume-box' });
            this.volumeSliderActive = new Element('div', { 'class': 'slider-active' });
            this.volumeSliderInactive = new Element('div', { 'class': 'slider-inactive' });
            this.volumeBoxOverlay = new Element('div', { 'class': 'overlay' });
            
            
            Event.observe(this.playPauseButton, 'click', (function(e) {
                // The status icon is changed in the setAsPlaying or setAsPaused
                // methods invoked by such events as onplay and onpause
                if (this.video.paused) {
                    this.video.play();
                } else {
                    this.video.pause();
                }
            }).bindAsEventListener(this));
            
            Event.observe(this.closeButton, 'click', (function(e) {
                this.showcase.hideVideoController(true);
                this.video.pause();
                this.showcase.selectedContent.delegate.showOverlayControl();
            }).bindAsEventListener(this));
            
            Event.observe(this.seekBar, 'mousedown', (function(e) {
                // Observe global event of mouse up to mark it is released
                var mouseUpHandler = (function(e) {
                    Event.stopObserving(window, 'mouseup', mouseUpHandler);
                    Event.stopObserving(window, 'mousemove', mouseMoveHandler);
                }).bindAsEventListener(this);
                
                var mouseMoveHandler = (function(e) {
                    // Seek the video
                    this.video.currentTime = this.video.duration * ((e.pointerX() - this.seekBar.cumulativeOffset().left - seekBarOffset) / (seekBarWidth - seekBarOffset * 2));
                }).bindAsEventListener(this);
                
                Event.observe(window, 'mouseup', mouseUpHandler);
                Event.observe(window, 'mousemove', mouseMoveHandler);
                
                // Seek the video
                this.video.currentTime = this.video.duration * ((e.pointerX() - this.seekBar.cumulativeOffset().left - seekBarOffset) / (seekBarWidth - seekBarOffset * 2));
            }).bindAsEventListener(this));
            
            Event.observe(this.volumeButton, 'mouseover', (function(e) {
                this.volumeBox.show();
                this.volumeBox.setStyle({
                    left : this.volumeButton.positionedOffset().left + 'px',
                    top  : this.volumeButton.positionedOffset().top + 'px'
                });
                this.volumeBox.addClassName('focused');
                this.volumeButton.addClassName('focused');
                this._volumeBoxFocused = true;
            }).bindAsEventListener(this));
            
            Event.observe(this.volumeBoxOverlay, 'mousedown', (function(e) {
                this._volumeBoxFocused = true;
            }).bindAsEventListener(this));
            
            Event.observe(this.volumeBoxOverlay, 'mouseout', (function(e) {
                if (!this._volumeBoxPressed) {
                    defocusVolumeBox.bind(this)();
                }
                this._volumeBoxFocused = false;
            }).bindAsEventListener(this));
    
            Event.observe(this.volumeBoxOverlay, 'mousedown', (function(e) {
                // Observe global event of mouse up to mark it is released
                var mouseUpHandler = (function(e) {
                    if (!this._volumeBoxFocused) {
                        defocusVolumeBox.bind(this)();
                    }
                    Event.stopObserving(window, 'mouseup', mouseUpHandler);
                    Event.stopObserving(window, 'mousemove', mouseMoveHandler);
                    this._volumeBoxPressed = false;
                }).bindAsEventListener(this);
                
                var mouseMoveHandler = (function(e) {
                    // Update the volume value
                    this.setVolume((e.pointerX() - this.volumeBoxOverlay.cumulativeOffset().left - volumeSliderOffset) / volumeSliderWidth);
                }).bindAsEventListener(this);
                
                Event.observe(window, 'mouseup', mouseUpHandler);
                Event.observe(window, 'mousemove', mouseMoveHandler);
                
                // Update the volume value
                this.setVolume((e.pointerX() - this.volumeBoxOverlay.cumulativeOffset().left - volumeSliderOffset) / volumeSliderWidth);
                
                this._volumeBoxPressed = true;
            }).bindAsEventListener(this));
            
            
            // Make structure
            this.seekBar.insert(this.seekActive);
            this.seekBar.insert(this.seekInactive);
            this.seekBar.insert(this.seekThumb);
            this.volumeBox.insert(this.volumeSliderActive);
            this.volumeBox.insert(this.volumeSliderInactive);
            this.volumeBox.insert(this.volumeBoxOverlay);
            this.volumeBox.hide();
            this.element.insert(this.closeButton);
            this.element.insert(this.playPauseButton);
            this.element.insert(this.seekBar);
            this.element.insert(this.volumeButton);
            this.element.insert(this.volumeBox);
            
            // Preload critical images
            ['/img/showcase_controller_play.png',
            '/img/showcase_controller_pause.png',
            '/img/showcase_controller_close.png',
            '/img/showcase_controller_volume_mute.png',
             '/img/showcase_controller_volume_min.png',
             '/img/showcase_controller_volume_mid.png',
             '/img/showcase_controller_volume_max.png',
             '/img/showcase_controller_seekbar.png',
             '/img/showcase_controller_thumb.png',
            '/img/showcase_controller_volume_slider.png'].each(function(url) {
                (new Image()).src = url;
            });
            
            this.setSeekPosition(0);
            this.setBufferedTime(0);
            this.setAsPaused();
        }, 
        
        setVideo: function(video, status) {
            this.video = this.showcase.selectedContent.delegate.video;
            status = status || {};
            this.setBufferedTime(status.bufferedTime || 0);
            this.setSeekPosition(status.seekPosition || 0);
            
            if (this.showcase.selectedContent.delegate.options.hideVolume) {
                this.volumeButton.hide();
            } else {
                this.volumeButton.show();
            }
        },
        
        setAsPlaying: function() {
            this.playPauseButton.removeClassName('play');
            this.playPauseButton.addClassName('pause');
            this.playPauseButton.writeAttribute('title', 'Pause');
        },
        
        setAsPaused: function() {
            this.playPauseButton.addClassName('play');
            this.playPauseButton.removeClassName('pause');
            this.playPauseButton.writeAttribute('title', 'Play');
        },
        
        setBufferedTime: function(value) {
            // Move the seekbar by changing width of the active and inactive 
            // elements
            var activeWidth = Math.round(seekBarOffset + (seekBarWidth - seekBarOffset * 2) * value);
            this.seekActive.setStyle({ width: activeWidth + 'px' });
            this.bufferedTime = value;
            if (value == 1) {
                this.seekActive.setStyle({ width: seekBarWidth + 'px' });
            }
        },
        
        setSeekPosition: function(value) {
            this.seekPosition = Math.min(1, Math.max(0, value));
            this.seekThumb.setStyle({ left: Math.round((seekBarWidth - seekBarOffset * 2) * this.seekPosition) + 'px' });
        },
        
        setVolume: function(value) {
            this.volume = Math.min(1, Math.max(0, value));
            this.video.volume = this.volume;
            
            // Move the slider by changing width of the active and inactive 
            // elements
            var activeWidth = Math.round(volumeSliderWidth * this.volume);
            this.volumeSliderActive.setStyle({ width: activeWidth + 'px' });
            this.volumeSliderInactive.setStyle({ width: volumeSliderWidth - activeWidth + 'px' });
            
            // Change the volume icon to the corresponding value
            this.volumeButton.removeClassName('mute');
            this.volumeButton.removeClassName('min');
            this.volumeButton.removeClassName('mid');
            this.volumeButton.removeClassName('max');
            if (this.volume == 0) {
                this.volumeButton.addClassName('mute');
            } else if (this.volume < 1 / 3) {
                this.volumeButton.addClassName('min');
            } else if (this.volume < 2 / 3) {
                this.volumeButton.addClassName('mid');
            } else {
                this.volumeButton.addClassName('max');
            }
            
            // Save the volume to cookie
            writeCookie('videoVolume', this.volume);
        }, 
        
        getStatus: function() {
            return {
                volume: this.volume,
                bufferedTime: this.bufferedTime,
                seekPosition: this.seekPosition
            }
        }
    };
})());




/* ---------- Showcase Content Delegates ------------------------------------ */

Sgss.Showcase.Delegate = Class.create({
    interruptTransition: false,
    
    initialize: function(showcase, content, options) {
        this.showcase = showcase;
        this.content = content;
        this.options = options || {};
    },
        
    updateDimensions: function(e) {
        var parent = this.element.up();
        this.element.setStyle({
            width  : parent.getWidth() + 'px',
            height : parent.getHeight() + 'px'
        });
    }
});

Object.extend(Sgss.Showcase.Delegate, {
    create: function(showcase, content) {
        var klass = content.type.capitalize();
        return new Sgss.Showcase.Delegate[klass](showcase, content, content.options);
    }
});

/* ---------- Delegate.Image ----------*/

Sgss.Showcase.Delegate.Image = Class.create(Sgss.Showcase.Delegate, (function() {
    var copyrightHeight = 27;
    
    return {
        initialize: function($super, showcase, content, options) {
            $super(showcase, content, options);
            
            this.element = new Element('img', { src: content.url });
        },
        
        updateDimensions: function(e) {
            var parent = this.element.up();
            this.element.setStyle({
                width  : parent.getWidth() + 'px',
                height : parent.getHeight() + (this.options.copyrightHeight || copyrightHeight) + 'px'
            });
        }
    };
})());

/* ---------- Delegate.Video ----------*/

Sgss.Showcase.Delegate.Video = Class.create(Sgss.Showcase.Delegate, (function() {
    var copyrightHeight = 27;
    
    return {
        initialize: function($super, showcase, content, options) {
            $super(showcase, content, options);
            
            this.element = new Element('div', { 'class': 'video' });
            this.posterElement = new Element('img', { 'class': 'poster', src: options.poster });
            this.overlayElement = new Element('div', { 'class': 'overlay' });
            this.overlayControlElement = new Element('div', { 'class': 'overlay-control' });
            
            Event.observe(this.overlayControlElement, 'mouseover', (function(e) {
                this.overlayElement.addClassName('active');
            }).bindAsEventListener(this));
            
            Event.observe(this.overlayControlElement, 'mouseout', (function(e) {
                this.overlayElement.removeClassName('active');
            }).bindAsEventListener(this));
            
            Event.observe(this.overlayControlElement, 'click', (function(e) {
                if (!this._loadStarted) {
                    // Video load starts here. We must insert these elements
                    // only once
                    this.showcase.videoController.setVideo(this.video);
                    this.hideOverlayControl();
                    this.posterElement.hide();
                    this.showcase.showVideoController(true);
                    this.video.insert(new Element('source', { src: swapExtension(content.url, 'mp4'),  type: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"' }));
                    this.video.insert(new Element('source', { src: swapExtension(content.url, 'webm'), type: 'video/webm; codecs="vp8, vorbis"' }));
                    this.video.insert(new Element('source', { src: swapExtension(content.url, 'ogg'),  type: 'video/ogg; codecs="theora, vorbis"' }));
                    this.video.load();
                    this._loadStarted = true;
                
                } else {
                    this.showcase.videoController.setVideo(this.video, this._statusOnClosed);
                    if (this._statusOnClosed) {
                        if (this._statusOnClosed.currentTime) {
                            this.video.currentTime = this._statusOnClosed.currentTime;
                        }
                    }
                    this.showcase.showVideoController(true);
                    this.video.play();
                }
            }).bindAsEventListener(this));
            
            this.video = new Element('video');
            this.video.hide();
            
            Event.observe(this.video, 'loadeddata', (function(e) {
                this.video.show();
                // Set the volume to the value stored in cookie
                this.showcase.videoController.setVolume(readCookie('videoVolume') || (1 / 3));
            }).bindAsEventListener(this));
            
            Event.observe(this.video, 'canplay', (function(e) {
                if (!this._statusOnClosed) {
                    // Video is loaded for the first time by clicking the 
                    // overlay control: play it
                    this.video.play();
                }
            }).bindAsEventListener(this));
            
            Event.observe(this.video, 'play', (function(e) {
                this.showcase.videoController.setAsPlaying();
                if (!this._overlayControlHidden) {
                    this.hideOverlayControl();
                }
            }).bindAsEventListener(this));
            
            Event.observe(this.video, 'pause', (function(e) {
                this.showcase.videoController.setAsPaused();
            }).bindAsEventListener(this));
            
            Event.observe(this.video, 'progress', (function(e) {
                this.updateBufferedTime();
            }).bindAsEventListener(this));
            
            Event.observe(this.video, 'load', (function(e) {
                this.showcase.videoController.setBufferedTime(1);
            }).bindAsEventListener(this));
            
            Event.observe(this.video, 'timeupdate', (function(e) {
                var value = this.video.currentTime / this.video.duration;
                this.showcase.videoController.setSeekPosition(value);
                this.updateBufferedTime();
            }).bindAsEventListener(this));
            
            Event.observe(this.video, 'ended', (function(e) {
                this.showcase.videoController.setAsPaused();
            }).bindAsEventListener(this));
            
            this.element.insert(this.posterElement);
            this.element.insert(this.overlayElement);
            this.element.insert(this.overlayControlElement);
            this.element.insert(this.video);
        },
        
        updateBufferedTime: function() {
            var value = 0;
            if (this.video.buffered.length > 0) {
                value = this.video.buffered.end(this.video.buffered.length - 1);
            }
            this.showcase.videoController.setBufferedTime(value / this.video.duration);
        }, 
        
        showOverlayControl: function() {
            this.overlayElement.show();
            this.overlayControlElement.show();
            this._overlayControlHidden = false;
            this._statusOnClosed = this.showcase.videoController.getStatus();
        }, 
        
        hideOverlayControl: function() {
            this.overlayElement.hide();
            this.overlayControlElement.hide();
            this._overlayControlHidden = true;
        }, 
        
        beforeBecomeVisible: function(animated) {
            this.overlayElement.show();
            this.overlayControlElement.show();
            this._overlayControlHidden = false;
        },
        
        beforeBecomeHidden: function(animated) {
            // Back the poster control to the normal depth
            this.overlayControlElement.setStyle({ zIndex: 0 });
            this.showcase.hideVideoController(animated);
            this._statusOnClosed = this.showcase.videoController.getStatus();
            Object.extend(this._statusOnClosed, {
                currentTime: this.video.currentTime
            });
        },
        
        afterBecomeVisible: function(animated) {
            // Bring the poster control to the top of showcase
            this.overlayControlElement.setStyle({ zIndex: 100 });
        },
        
        updateDimensions: function(e) {
            var parent = this.element.up();
            this.posterElement.setStyle({
                width  : parent.getWidth() + 'px',
                height : parent.getHeight() + (this.options.copyrightHeight || copyrightHeight) + 'px'
            });
            this.video.setStyle({
                width  : parent.getWidth() + 2 + 'px',
                height : parent.getHeight() + 2 + 'px'
            });
            this.overlayControlElement.setStyle({
                left : Math.round((parent.getWidth() - this.overlayControlElement.getWidth()) / 2) + 'px',
                top  : Math.round((parent.getHeight() - this.overlayControlElement.getHeight()) / 2) + 'px',
            });
        }
    };
})());

/* ---------- Delegate.Flash ----------*/

Sgss.Showcase.Delegate.Flash = Class.create(Sgss.Showcase.Delegate, (function() {
    var flashParams = {
        quality           : 'best',
        bgcolor           : '#ffffff',
        play              : 'true',
        loop              : 'true',
        wmode             : 'opaque',
        scale             : 'showall',
        menu              : 'true',
        devicefont        : 'false',
        salign            : '',
        allowscriptaccess : 'sameDomain'
    };
    
    return {
        initialize: function($super, showcase, content, options) {
            $super(showcase, content, options);
            
            // A little trick here: make a child and call createSWF to it, then, 
            // return the child, otherwise the function does not work...
            var parent = new Element('div');
            var child = new Element('div');
            parent.insert(child);
            var attributes = {
                id    : content.hashName,
                name  : content.hashName,
                align : 'middle'
            };
            swfobject.createSWF(
                'swf/' + content.hashName + '.swf', 
                child,      // element
                '100%',     // width
                '100%',     // height
                '10.1.52',  // swfVersionStr
                '',         // xiSwfUrlStr
                {},         // flashvars 
                flashParams, 
                attributes
            );
            this.element = parent.childNodes[0];
        }
    };
})());

/* ---------- Delegate.Digest ----------*/

Sgss.Showcase.Delegate.Digest = Class.create(Sgss.Showcase.Delegate, (function() {
    var copyrightHeight = 27;
    var animationDuration = 0.3;
    var sidenoteAnimationDelay = 1;
    var annotationAnimationDelay = 0.1;
    var movementLength = 300;
        
    function sidenoteAnimationTimerFired() {
        if (this._annotationImageLoaded) {
            new Effect.Move(this.sidenoteElement, {
                x           : 0,
                mode        : 'absolute',
                duration    : (this.options.animationDuration || animationDuration)
            });
            new Effect.Move(this.annotationElement, {
                x        : 0,
                mode     : 'absolute',
                duration : (this.options.animationDuration || animationDuration),
                delay    : annotationAnimationDelay
            });
            this.__sidenoteAnimationTimer = null;
            // Also clear the interval timer if it is set
            if (this.__sidenoteAnimationIntervalTimer) {
                clearInterval(this.__sidenoteAnimationIntervalTimer);
                this.__sidenoteAnimationIntervalTimer = null;
            }
            
        // Wait for the annotation image to be loaded if the image is not 
        // loaded when the animation timer is fired
        } else if (!this.__sidenoteAnimationIntervalTimer) {
            this.__sidenoteAnimationTimer = null;
            this.__sidenoteAnimationIntervalTimer = setInterval(sidenoteAnimationTimerFired.bind(this), 100);
        }
    }
    
    return {
        initialize: function($super, showcase, content, options) {
            $super(showcase, content, options);
            
            this.element = new Element('div', { 'class': 'digest' });
            
            // Background
            this.backgroundElement = new Element('img', { src: content.url });
            this.backgroundElement.addClassName('background');
            // Sidenote
            this.sidenoteElement = new Element('div', { 'class': 'sidenote' });
            this.sidenoteElement.setStyle({ 
                backgroundColor: this.options.color,
                opacity: this.options.opacity || 1.0
            });
            // Annotation
            this.annotationElement = new Element('div', { 'class': 'annotation' });
            this.annotationElement.setStyle({ backgroundImage: 'url("/img/annotation_' + this.options.hashName + '.png")' });
            
            this.element.insert(this.backgroundElement);
            this.element.insert(this.sidenoteElement);
            this.element.insert(this.annotationElement);
            
            if (this.options.href) {
                // Sidenote Overlay
                this.sidenoteOverlayElement = new Element('div', { 'class': 'overlay' });
                // Overlay
                this.overlayElement = new Element('a', { 'class': 'overlay', href: this.options.href });
                
                Event.observe(this.overlayElement, 'mouseover', this.sidenoteOverlayElement.addClassName.bind(this.sidenoteOverlayElement, 'active'));
                Event.observe(this.overlayElement, 'mouseout', this.sidenoteOverlayElement.removeClassName.bind(this.sidenoteOverlayElement, 'active'));
                
                this.sidenoteElement.insert(this.sidenoteOverlayElement);
                this.element.insert(this.overlayElement);
                
                // This tells the showcase not to make transition while the user
                // is interacting with the showcase
                this.interruptTransition = true;
            }
            
            // Preload annotation image 
            this.annotationImage = new Image();
            this.__annotationImageLoadListener = (function(e) {
                Event.stopObserving(this.annotationImage, 'load', this.__annotationImageLoadListener);
                this._annotationImageLoaded = true;
                // Update dimensions so that the annotation image will be 
                // centered before it becomes visible
                this.updateDimensions();
            }).bindAsEventListener(this);
            Event.observe(this.annotationImage, 'load', this.__annotationImageLoadListener);
            this.annotationImage.src = '/img/annotation_' + this.options.hashName + '.png';
        },
        
        beforeBecomeVisible: function(animated) {
            this.updateDimensions();
            
            // Move the elements to the right of the showcase
            this.sidenoteElement.setStyle({ left: -movementLength + 'px' });
            this.annotationElement.setStyle({ left: -movementLength + 'px' });
        },
        
        afterBecomeVisible: function(animated) {
            this.__sidenoteAnimationTimer = setTimeout(sidenoteAnimationTimerFired.bind(this), sidenoteAnimationDelay * 1000);
        },
        
        beforeBecomeHidden: function(animated) {
            // Invalidate all timers
            if (this.__sidenoteAnimationTimer) {
                clearTimeout(this.__sidenoteAnimationTimer);
            }
            if (this.__sidenoteAnimationIntervalTimer) {
                clearInterval(this.__sidenoteAnimationIntervalTimer);
            }
        },
        
        updateDimensions: function(e) {
            var parent = this.element.up();
            this.backgroundElement.setStyle({
                width  : parent.getWidth() + 'px',
                height : parent.getHeight() + (this.options.copyrightHeight || copyrightHeight) + 'px'
            });
            // Change only the height of the elements
            this.sidenoteElement.setStyle({ height : parent.getHeight() + 'px' });
            this.annotationElement.setStyle({ height : parent.getHeight() + 'px' });
            
            if (this._annotationImageLoaded) {
                // Position the annotation image at the *visual* center of the 
                // showcase
                this.annotationElement.setStyle({ backgroundPosition: '0 ' + Math.round((parent.getHeight() - this.annotationImage.height) / 2 + this.annotationImage.height / 5) + 'px'});
            }
        }
    };
})());

/* ---------- Showcase.Frame ------------------------------------------------- */

Sgss.Showcase.Frame = Class.create((function() {
    // Positions for all corners
    var positions = [{fromLeft: true,  fromTop: true},
                     {fromLeft: false, fromTop: true},
                     {fromLeft: false, fromTop: false},
                     {fromLeft: true,  fromTop: false}];
    // Parameters for every pixel of the corner masks
    var cornerMask = [{x: 1, y: 0, width: 3, height: 1, alpha: 1.0},
                      {x: 0, y: 0, width: 1, height: 4, alpha: 1.0},
                      {x: 1, y: 1, width: 1, height: 1, alpha: 1.0},
                      {x: 1, y: 2, width: 1, height: 1, alpha: 0.6},
                      {x: 2, y: 1, width: 1, height: 1, alpha: 0.6}];
    
    function createPixelElement(params, fromLeft, fromTop) {
        var element = document.createElement('div');
        var style = {
            position         : 'absolute',
            backgroundColor  : document.body.getStyle('background-color'),
            backgroundImage  : document.body.getStyle('background-image'),
            backgroundRepeat : 'no-repeat',
            zIndex           : 10,
            width            : params.width + 'px',
            height           : params.height + 'px',
            opacity          : params.alpha
        };
        style[fromLeft ? 'left' : 'right'] = params.x + 'px';
        style[fromTop  ? 'top' : 'bottom'] = params.y + 'px';
        element.setStyle(style);
        return element;
    }
    
    return {
        initialize: function(element, options) {
            this.element = $(element);
            this.options = options || {};
            this.pixelElements = [];
            
            // Make container elements of rounded mask that fills the showcase 
            // container
            this.element.insert(divc('corner-topright', 'corner-topleft', 
                                     'corner-bottomright', 'corner-bottomleft',
                                     'fill-top', 'fill-right', 'fill-bottom', 
                                     'fill-left'));
            // Add rounded corner masks into the container
            cornerMask.each(function(params) {
                positions.each(function(position) {
                    var pixelElement = createPixelElement(params, position.fromLeft, position.fromTop);
                    this.element.insert(pixelElement);
                    this.pixelElements.push(pixelElement);
                 }, this);
            }, this);
            
            this.updateDimensions();
            Event.observe(window, 'resize', this.updateDimensions.bindAsEventListener(this));
        },
    
        updateDimensions: function(e) {
            // Adjust the background style of the container elements just after 
            // the interpreter becomes idle (calling defer)
            (function() {
                this.pixelElements.each(function(element) {
                    element.setStyle({ backgroundPosition: '0 -' + element.cumulativeOffset().top + 'px' });
                });
            }).bind(this).defer();
        }
    };
})());

/* ---------- Column -------------------------------------------------------- */

Sgss.Column = Class.create((function() {
    var indexImageDimensions = {
        width: 500,
        height: 150
    };
    
    return {
        initialize: function(container, options) {
            this.container = $(container);
            this.options = options || {};
            this.columnElements = container.select('.column');
            
            // Break float of the element
            container.insert('<div style="clear:left;"></div>');
            // Add frame if the element has showcase child
            // (typically used in the index page of each category)
            container.select('div.showcase').each(function(column) {
                new Sgss.Showcase.Frame(column);
            });
            
            this.updateDimensions();
            Event.observe(window, 'resize', this.updateDimensions.bindAsEventListener(this));
        },
        
        updateDimensions: function(e) {
            var columnSpacing = this.options.columnSpacing || 0;
            var numColumns = this.columnElements.length;
            var columnWidth = ($('body').getWidth() - columnSpacing * (numColumns - 1)) / numColumns;
            var columnCursor = 0;
            
            this.columnElements.each(function(column, i) {
                // Apply the column width and margin to every column except the last 
                // one
                column.setStyle({ width: Math.round(columnCursor + columnWidth) - Math.round(columnCursor) + 'px' });
                if (i < numColumns - 1) {
                    column.setStyle({ marginRight: columnSpacing + 'px' });
                }
                columnCursor += columnWidth + columnSpacing;
                
                // Handle alignment of showcase image placed inside a column
                var showcase = column.select('div.showcase').first();
                if (showcase) {
                    var image = showcase.select('img').first();
                    if (image) {
                        if (showcase.hasClassName('left-align')) {
                            image.setStyle({ left: 0 });
                        } else if (showcase.hasClassName('right-align')) {
                            image.setStyle({ right: 0 });
                        } else {
                            image.setStyle({ right: Math.round((columnWidth - indexImageDimensions.width) / 2) + 'px' });
                        }
                    }
                }
            }, this);
        }
    };
})());

/* ---------- Separator ----------------------------------------------------- */

Sgss.Separator = Class.create({
    initialize: function(element, options) {
        this.element = $(element);
        this.options = options || {};
        this.element.insert(divc('fill', 'cap-right', 'cap-left'));
    }
});

})();

