/*
Copyright (c) 2007-2010 the OTHER media Limited
Licensed under the BSD license, http://ojay.othermedia.org/license.html
Version: master-be01d9867e00e3011c1a83198af9520eab9e297e
Build:   source
*/

/**
 * @module Ojay.Paginatable
 */
require('Ojay', function() {
	Ojay.Paginatable = new JS.Module('Ojay.Paginatable', {
	    include: [Ojay.Observable, JS.State],
    
	    /**
	     * @returns {Object}
	     */
	    getInitialState: function() {
	        return {page: 1};
	    },
    
	    /**
	     * @param {Object} state
	     * @param {Function} callback
	     * @param {Object} scope
	     * @returns {Paginator}
	     */
	    changeState: function(state, callback, scope) {
	        if (state.page !== undefined) this._handleSetPage(state.page, callback, scope);
	        return this;
	    },
    
	    /**
	     * <p>Returns the direction of the paginator.</p>
	     * @returns {String}
	     */
	    getDirection: function() {
	        return this._options.direction;
	    },
    
	    /**
	     * <p>Returns a boolean to indicate whether the paginator loops.</p>
	     * @returns {Boolean}
	     */
	    isLooped: function() {
	        return !!this._options.looped || !!this._options.infinite;
	    },
    
	    /**
	     * <p>Returns an Ojay collection wrapping the wrapper element added to your document to
	     * contain the original content element and let it slide.</p>
	     * @returns {DomCollection}
	     */
	    getContainer: function() {
	        return this.getHTML();
	    },
    
	    /**
	     * <p>Returns an Ojay collection wrapping the sliding element, i.e. the element you specify
	     * when creating the <tt>Paginator</tt> instance.</p>
	     * @returns {DomCollection}
	     */
	    getSubject: function() {
	        return this._elements._subject || undefined;
	    },
    
	    /**
	     * <p>Returns a <tt>Region</tt> object representing the area of the document occupied by
	     * the <tt>Paginator</tt>'s container element.</p>
	     * @returns {Region}
	     */
	    getRegion: function() {
	        if (!this._elements._container) return undefined;
	        return this._elements._container.getRegion();
	    },
    
	    /**
	     * <p>Returns the number of the current page, numbered from 1.</p>
	     * @returns {Number}
	     */
	    getCurrentPage: function() {
	        return this._currentPage || undefined;
	    },
    
	    /**
	     * @returns {Number}
	     */
	    getCurrentOffset: function() {
	        return this._reportedOffset;
	    },
    
	    /**
	     * <p>Places a default set of UI controls before or after the <tt>Paginator</tt> in the
	     * document and returns a <tt>Paginator.Controls</tt> instance representing this UI.</p>
	     * @returns {Paginator.Controls}
	     */
	    addControls: function(position) {
	        if (this.inState('CREATED') || !/^(?:before|after)$/.test(position)) return undefined;
	        var controls = new Ojay.Paginator.Controls(this);
	        this.getContainer().insert(controls.getHTML().node, position);
	        return controls;
	    },
    
	    states: {
	        CREATED: {
	            /**
	             * <p>Sets the initial page for the paginator to start at when in the CREATED
	             * state. No scrolling takes place, and the number set will override the initial
	             * page setting and any setting pulled in by the history manager.</p>
	             * @param {Number} page
	             * @returns {Paginator}
	             */
	            setPage: function(page) {
	                this._currentPage = Number(page);
	                return this;
	            }
	        },
        
	        READY: {
	            /**
	             * <p>Sets the current page of the <tt>Paginator</tt> by scrolling the subject
	             * element. Will fire a <tt>pagechange</tt> event if the page specified is not
	             * equal to the current page.</p>
	             * @param {Number} page
	             * @param {Function} callback
	             * @param {Object} scope
	             * @returns {Paginator}
	             */
	            setPage: function(page, callback, scope) {
	                var pages = this.getPages(), page = Number(page);
	                if (this._options.looped && page < 1) page += pages;
	                if (this._options.looped && page > pages) page -= pages;
                
	                if (!this.isLooped() && (page == this._currentPage || page < 1 || page > pages))
	                    return this;
                
	                this.changeState({page: page}, callback, scope);
	                return this;
	            },
            
	            /**
	             * <p>Sets the scroll offset of the subject element. If <tt>amount</tt> is between
	             * 0 and 1, it is taken as a fraction of the total offset. If it is greater than 1,
	             * it is taken as an absolute pixel value. The options hash may specify any of
	             * the following:</p>
	             * 
	             * <ul>
	             *     <li><tt>animate</tt>: sets whether the scroll move should be animated</li>
	             *     <li><tt>silent</tt>: set to <tt>true</tt> to prevent any <tt>scroll</tt> events from firing</li>
	             *     <li><tt>absolute</tt>: if <tt>true</tt> prevents values 0 to 1 being scaled up</li>
	             *     <li><tt>clip</tt>: set to <tt>false</tt> to stop offsets being clipped to the scroll limits</li>
	             * </ul>
	             * 
	             * <p>This method is largely for internal use and is a little messy due to all
	             * the behaviour it must support: overshooting filmstrips, infinite looped pagination,
	             * relative values, and page-change detection for firing events. I would refactor
	             * but this really should all be one atomic action.</p>
	             * 
	             * @param {Number} amount
	             * @param {Object} options
	             * @param {Function} callback
	             * @param {Object} scope
	             * @returns {Paginatable}
	             */
	            setScroll: function(amount, options, callback, scope) {
	                var options    = options || {},
	                    scrollTime = options._scrollTime || this._options.scrollTime,
	                    vertical   = (this._options.direction == 'vertical'),
	                    limits     = this.getScrollLimits(),
	                    total      = limits[1] - limits[0],
	                    chain      = new JS.MethodChain(),
	                    settings;
                
	                // Scale up values between 0 and 1
	                if (amount >= 0 && amount <= 1 && !options.absolute) amount = limits[0] + amount * total;
	                this._reportedOffset = amount;
                
	                // Clip to the object's scroll offset limits
	                if (options.clip !== false) amount = Math.min(Math.max(amount, limits[0]), limits[1]);
                
	                // Either animate or go straight to the new offset
	                if (options.animate && YAHOO.util.Anim) {
	                    this.setState('SCROLLING');
	                    settings = vertical
	                            ? { top: {to: -amount} }
	                            : { left: {to: -amount} };
	                    this._elements._subject.animate(settings,
	                        scrollTime, {easing: this._options.easing})._(function(self) {
	                        self.setState('READY');
	                        chain.fire(scope || self);
	                        if (callback) callback.call(scope || null);
	                    }, this);
	                } else {
	                    settings = vertical
	                            ? { top: (-amount) + 'px' }
	                            : { left: (-amount) + 'px' };
	                    this._elements._subject.setStyle(settings);
	                }
                
	                // Calculate relative offset between 0 and 1 for event listeners
	                var reportedOffset = (amount - limits[0])/total;
	                if (reportedOffset < 0) reportedOffset = 1;
	                if (reportedOffset > 1) reportedOffset = 0;
                
	                if (!options.silent) this.notifyObservers('scroll', reportedOffset, total);
                
	                // Detect page changes
	                var pages = this.getPages(),
	                    page  = this._pageFromOffset(this._reportedOffset);
                
	                if (page != this._currentPage) {
	                    this._currentPage = page;
	                    this.notifyObservers('pagechange', page);
                    
	                    if (page == 1) this.notifyObservers('firstpage');
	                    if (page == pages) this.notifyObservers('lastpage');
	                }
                
	                return (options.animate && YAHOO.util.Anim) ? chain : this;
	            }
	        },
        
	        SCROLLING: {}
	    }
	});


	/**
	 * <p>The <tt>Paginator</tt> class is used to replace large blocks of content with a smaller,
	 * scrollable area with an API for controlling the area. The content will typically be made up
	 * of series of items of the same size that can be grouped into pages. For example, an image
	 * gallery could be set up as a series of floated divs or a list...</p>
	 *
	 * <pre><code>    &lt;div id="gallery"&gt;
	 *         &lt;div class="item"&gt;&lt;img src="01.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="02.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="03.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="04.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="05.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="06.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="07.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="08.jpg" /&gt;&lt;/div&gt;
	 *     &lt;/div&gt;</code></pre>
	 *
	 * <p>A <tt>Paginator</tt>, when applied to <tt>#gallery</tt>, will wrap its child elements in
	 * a scrollable element that can be controlled using the <tt>Paginator</tt> API. So, your markup
	 * will now look like:</p>
	 *
	 * <pre><code>    &lt;div class="paginator"&gt;
	 *         &lt;div id="gallery"&gt;
	 *             &lt;div class="page"&gt;
	 *                 &lt;div class="item"&gt;&lt;img src="01.jpg" /&gt;&lt;/div&gt;
	 *                 &lt;div class="item"&gt;&lt;img src="02.jpg" /&gt;&lt;/div&gt;
	 *                 &lt;div class="item"&gt;&lt;img src="03.jpg" /&gt;&lt;/div&gt;
	 *             &lt;/div&gt;
	 *             &lt;div class="page"&gt;
	 *                 &lt;div class="item"&gt;&lt;img src="04.jpg" /&gt;&lt;/div&gt;
	 *                 &lt;div class="item"&gt;&lt;img src="05.jpg" /&gt;&lt;/div&gt;
	 *                 &lt;div class="item"&gt;&lt;img src="06.jpg" /&gt;&lt;/div&gt;
	 *             &lt;/div&gt;
	 *             &lt;div class="page"&gt;
	 *                 &lt;div class="item"&gt;&lt;img src="07.jpg" /&gt;&lt;/div&gt;
	 *                 &lt;div class="item"&gt;&lt;img src="08.jpg" /&gt;&lt;/div&gt;
	 *             &lt;/div&gt;
	 *         &lt;/div&gt;
	 *     &lt;/div&gt;</code></pre>
	 *
	 * <p>The outer element is referred to as the 'container', and the inner element the 'subject'.
	 * <tt>Paginator</tt> objects publish a number of events -- they are as follows:</p>
	 *
	 * <ul>
	 *      <li><tt>pagechange</tt> - when the current page number changes</li>
	 *      <li><tt>scroll</tt> when any scrolling takes place</li>
	 *      <li><tt>firstpage</tt> - when the paginator reaches the first page</li>
	 *      <li><tt>lastpage</tt> - when the paginator reaches the last page</li>
	 *      <li><tt>focusitem</tt> - when <tt>focusItem()</tt> is called</li>
	 *      <li><tt>pagecreate</tt> - when a new page is created</li>
	 *      <li><tt>pagedestroy</tt> - when a page is removed</li>
	 *      <li><tt>itemadd</tt> - when a new item is added</li>
	 *      <li><tt>itemremove</tt> - when an item is removed</li>
	 * </ul>
	 *
	 * <p>See the website for further documentation and graphical examples.</p> 
	 *
	 * @constructor
	 * @class Paginator
	 */
	Ojay.Paginator = new JS.Class('Ojay.Paginator', /** @scope Ojay.Paginator.prototype */{
	    include: Ojay.Paginatable,
    
	    extend: /** @scope Ojay.Paginator */{
	        CONTAINER_CLASS:    'paginator',
	        PAGE_CLASS:         'page',
	        ITEM_CLASS:         'item',
	        SCROLL_TIME:        0.5,
	        PUSH_FADE_TIME:     0.7,
	        PUSH_SLIDE_TIME:    0.3,
	        DIRECTION:          'horizontal',
	        EASING:             'easeBoth',
        
	        /**
	         * @param {Number} width
	         * @param {Number} height
	         * @returns {DomCollection}
	         */
	        makePageElement: function(width, height) {
	            var div = Ojay( Ojay.HTML.div({className: this.PAGE_CLASS}) );
	            div.setStyle({
	                'float': 'left', width: width + 'px', height: height + 'px',
	                margin: '0 0 0 0', padding: '0 0 0 0', border: 'none'
	            });
	            return div;
	        }
	    },
    
	    /**
	     * <p>To initialize, the <tt>Paginator</tt> instance needs a CSS selector and some configuration
	     * options. Available options are:</p>
	     *
	     * <ul>
	     *      <li><tt>width</tt> - the width as a string, in any units, e.g. '512px'.</li>
	     *      <li><tt>height</tt> - the height as a string, in any units, e.g. '512px'.</li>
	     *      <li><tt>rows</tt> - number of grid rows after pagination, alternative to <tt>height</tt>.</li>
	     *      <li><tt>columns</tt> - number of grid columns after pagination, alternative to <tt>width</tt>.</li>
	     *      <li><tt>scrollTime</tt> - the duration of the scoll effect in seconds.</li>
	     *      <li><tt>easing</tt> - sets the name of the easing effect to use.</li>
	     *      <li><tt>direction</tt> - 'horizontal' or 'vertical', sets scroll direction.</li>
	     *      <li><tt>pushFade</tt> - duration for fade-in animation when new items are added.</li>
	     *      <li><tt>pushSlide</tt> - duration for slide animation when new items are added.</li>
	     * </ul>
	     *
	     * @param {String|HTMLElement|DomCollection} subject
	     * @param {Object} options
	     */
	    initialize: function(subject, options) {
	        this._selector = subject;
	        this._elements = {};
        
	        options = this._options = options || {};
	        options.scrollTime = options.scrollTime || this.klass.SCROLL_TIME;
	        options.pushFade   = options.pushFade   || this.klass.PUSH_FADE_TIME;
	        options.pushSlide  = options.pushSlide  || this.klass.PUSH_SLIDE_TIME;
	        options.direction  = options.direction  || this.klass.DIRECTION;
	        options.easing     = options.easing     || this.klass.EASING;
	        options.looped     = !!options.looped;
	        options.infinite   = !!options.infinite;
        
	        this.setState('CREATED');
	    },
    
	    /**
	     * <p>Returns an Ojay collection wrapping all the HTML used by the paginator.</p>
	     * @returns {DomCollection}
	     */
	    getHTML: function() {
	        var elements = this._elements, options = this._options;
	        if (elements._container) return elements._container;
	        var container = Ojay( Ojay.HTML.div({className: this.klass.CONTAINER_CLASS}) );
	        container.addClass(this._options.direction);
        
	        var width = options.width, height = options.height, items;
	        if (options.rows || options.columns) {
	            items = this.getItems();
	            if (options.rows) height = (options.rows * items.getHeight()) + 'px';
	            if (options.columns) width = (options.columns * items.getWidth()) + 'px';
	        }
        
	        container.setStyle({
	            width:      width,
	            height:     height,
	            overflow:   'hidden',
	            padding:    '0 0 0 0',
	            border:     'none',
	            position:   'relative'
	        });
	        return elements._container = container;
	    },
    
	    /**
	     * @returns {Number}
	     */
	    getTotalOffset: function() {
	        var method = (this._options.direction == 'vertical') ? 'getHeight' : 'getWidth';
	        return this.getRegion()[method]() * (this._numPages - 1);
	    },
    
	    /**
	     * @returns {Array}
	     */
	    getScrollLimits: function() {
	        return [0, this.getTotalOffset()];
	    },
    
	    /**
	     * <p>Returns an Ojay collection wrapping the child elements of the subject.</p>
	     * @returns {DomCollection}
	     */
	    getItems: function() {
	        var elements = this._elements;
	        if (!elements._subject) return undefined;
	        if (elements._items) return elements._items;
	        elements._items = elements._subject.children(this._options.selector);
	        elements._items.setStyle({margin: '0 0 0 0'});
	        return elements._items;
	    },
    
	    /**
	     * <p>Returns the number of pages of content the <tt>Paginator</tt> has.</p>
	     * @returns {Number}
	     */
	    getPages: function() {
	        if (this._numPages) return this._numPages;
	        var items = this.getItems();
	        if (!items) return undefined;
	        if (items.length === 0) return 0;
	        var containerRegion = this.getRegion(), itemRegion = items.at(0).getRegion();
	        this._itemWidth     = itemRegion.getWidth();
	        this._itemHeight    = itemRegion.getHeight();
	        this._itemsPerCol   = (containerRegion.getHeight() / this._itemHeight).floor() || 1;
	        this._itemsPerRow   = (containerRegion.getWidth() / this._itemWidth).floor() || 1;
	        this._itemsPerPage  = this._itemsPerRow * this._itemsPerCol;
	        this._numPages = (items.length / this._itemsPerPage).ceil();
	        if (this._options.grouping !== false) this._groupItemsByPage();
	        return this._numPages;
	    },
    
	    /**
	     * <p>Returns the page corresponding to the given absolute offset.</p>
	     * @returns {Number}
	     */
	    _pageFromOffset: function(offset) {
	        offset = Math.min(Math.max(offset / this.getTotalOffset(), 0), 1);
	        return (this.getPages() * offset).ceil() || 1;
	    },
    
	    /**
	     * <p>Splits the list of item elements into groups by page, and wraps each group of items
	     * in a <tt>div</tt> that represents the page. This allows horizontal galleries to avoid
	     * stringing all the items onto one row.</p>
	     */
	    _groupItemsByPage: function() {
	        var containerRegion = this.getRegion(),
	            width           = containerRegion.getWidth(),
	            height          = containerRegion.getHeight(),
	            n               = this._itemsPerPage,
	            allItems        = this._elements._items.toArray();
        
	        this._elements._pages = [];
        
	        this._numPages.times(function(i) {
	            var items = allItems.slice(i * n, (i+1) * n);
	            var div = this.klass.makePageElement(width, height);
	            items.forEach(div.method('insert'));
	            this._elements._pages.push(div);
	            this._elements._subject.insert(div.node);
	        }, this);
        
	        this._dummyPage = this.klass.makePageElement(width, height);
	    },
    
	    /**
	     * <p>Returns the page number containing the nth child element. Pages and items are
	     * both numbered from 1 upwards.</p>
	     * @param {Number} id
	     * @returns {Number}
	     */
	    pageForItem: function(id) {
	        if (!this._numPages) return undefined;
	        var n = this._elements._items.length;
	        if (id < 1 || id > n) return undefined;
	        return ((id - 1) / this._itemsPerPage).floor() + 1;
	    },
    
	    states: {
	        /**
	         * <p>The <tt>Paginator</tt> is in the CREATED state when it has been instantiated but
	         * none of its DOM interactions have taken place. This attachment is deferred to the
	         * <tt>setup()</tt> call so that object can be history-managed before its UI is set up.</p>
	         */
	        CREATED: /** @scope Ojay.Paginator.prototype */{
	            /**
	             * <p>Sets up all the DOM changes the <tt>Paginator</tt> needs. If you want to history
	             * manage the object, make sure you set up history management before calling this method.
	             * Moves the object to the READY state if successful.</p>
	             * @returns {Paginator}
	             */
	            setup: function() {
	                var subject = this._elements._subject = Ojay(this._selector).at(0);
	                if (!subject.node) return this;
                
	                var container = this.getHTML();
	                subject.insert(container.node, 'after');
	                container.insert(subject.node);
	                subject.setStyle({padding: '0 0 0 0', border: 'none', position: 'absolute', left: 0, top: 0});
                
	                var pages = this._numPages = this.getPages(), region = this.getRegion();
                
	                var style = (this._options.direction == 'vertical')
	                        ? { width: region.getWidth() + 'px', height: (pages * region.getHeight() + 1000) + 'px' }
	                        : { width: (pages * region.getWidth() + 1000) + 'px', height: region.getHeight() + 'px' };
                
	                subject.setStyle(style);
                
	                var state = this.getInitialState();
	                this.setState('READY');
	                if (this._currentPage === undefined) this._currentPage = state.page;
	                this._handleSetPage(this._currentPage);
                
	                return this;
	            }
	        },
        
	        /**
	         * <p>The <tt>Paginator</tt> is in the READY state when all its DOM behaviour has been
	         * set up and it is not in the process of scrolling.</p>
	         */
	        READY: /** @scope Ojay.Paginator.prototype */{
	            /**
	             * <p>Handles request to <tt>changeState()</tt>.</p>
	             * @param {Number} page
	             * @param {Function} callback
	             * @param {Object} scope
	             */
	            _handleSetPage: function(page, callback, scope) {
	                var offset = this.getTotalOffset() * (page - 1) / (this._numPages - 1);
	                this.setScroll(offset, {animate: true, clip: false}, callback, scope);
	            },
            
	            /**
	             * <p>Increments the current page by one, firing a <tt>pagechange</tt> event.</p>
	             * @returns {Paginator}
	             */
	            incrementPage: function() {
	                var wrapping  = this._options.infinite && (this._currentPage == this._numPages),
	                    firstPage = this._elements._pages[0];
                
	                if (wrapping)
	                    this._elements._subject
	                      .insert(firstPage, 'bottom')
	                      .insert(this._dummyPage, 'top');
                
	                return this.setPage(this._currentPage + 1, function() {
	                    if (!wrapping) return;
	                    this._dummyPage.remove();
	                    this._elements._subject.insert(firstPage, 'top');
	                    this.setScroll(0, {animate: false, silent: true});
	                }, this);
	            },
            
	            /**
	             * <p>Decrements the current page by one, firing a <tt>pagechange</tt> event.</p>
	             * @returns {Paginator}
	             */
	            decrementPage: function() {
	                var wrapping = this._options.infinite && (this._currentPage == 1),
	                    property = (this._options.direction == 'vertical') ? 'marginTop' : 'marginLeft',
	                    lastPage = this._elements._pages[this._numPages - 1],
	                    settings = {};
                
	                if (wrapping) {
	                    this._elements._subject.insert(lastPage, 'top');
	                    settings[property] = (-this.getTotalOffset() / (this._numPages - 1)) + 'px';
	                    this._elements._subject.setStyle(settings);
	                }
                
	                return this.setPage(this._currentPage - 1, function() {
	                    if (!wrapping) return;
	                    this._elements._subject.insert(lastPage, 'bottom');
	                    settings[property] = 0;
	                    this._elements._subject.setStyle(settings);
	                    this.setScroll(1, {animate: false, silent: true});
	                }, this);
	            },
            
	            /**
	             * <p>Snaps the scroll offset of the <tt>Paginator</tt> to that of the current
	             * page. The optional <tt>animate</tt> parameter, if set to <tt>false</tt>, will
	             * prevent animation.</p>
	             * @param {Boolean} animate
	             * @returns {Paginator}
	             */
	            snapToPage: function(animate) {
	                this.setScroll((this._currentPage - 1) / (this._numPages - 1),
	                        {animate: animate !== false, silent: true});
	                return this;
	            },
            
	            /**
	             * <p>Scrolls to the page for the given item (numbered from 1) and adds a class
	             * off <tt>focused</tt> to that item's element.</p>
	             * @param {Number} id
	             * @returns {Paginator}
	             */
	            focusItem: function(id) {
	                var page = this.pageForItem(id);
	                if (!page) return this;
	                var element = this._elements._items.at(id - 1);
	                this.notifyObservers('focusitem', id, element);
	                this.setPage(page);
	                this._elements._items.removeClass('focused');
	                element.addClass('focused');
	                return this;
	            },
            
	            /**
	             * <p>Sets the scroll offset of the subject element. If <tt>amount</tt> is between
	             * 0 and 1, it is taken as a fraction of the total offset. If it is greater than 1,
	             * it is taken as an absolute pixel value. The options hash may specify <tt>animate</tt>,
	             * to say whether the scroll move should be animated, and <tt>silent</tt>, which if
	             * set to <tt>true</tt> will prevent any <tt>scroll</tt> events from firing.</p>
	             * @param {Number} amount
	             * @param {Object} options
	             * @param {Function} callback
	             * @param {Object} scope
	             * @returns {Paginator}
	             */
	            setScroll: function(amount, options, callback, scope) {
	                this._elements._items.removeClass('focused');
	                return this.callSuper();
	            },
            
	            /**
	             * <p>Pushes a new element onto the end of the list of elements contained in the
	             * <tt>Paginator</tt>, creating a new page and firing the <tt>pagecreate</tt>
	             * event if necessary. The <tt>n</tt> parameter is for internal use only, for when
	             * items need to be moved across page boundaries by <tt>shift</tt>/<tt>unshift</tt>
	             * operations.</p>
	             * @param {HTMLElement} element
	             * @param {Number} n
	             * @returns {Paginator}
	             */
	            push: function(element, n) {
	                n = (n === undefined) ? this._numPages - 1 : n;
	                var last = (n === this._numPages - 1);
	                if (last) this._checkPages();
                
	                element = Ojay(element).setStyle({margin: '0 0 0 0'});
	                var page = this._elements._pages[last ? this._numPages - 1 : n];
                
	                page.insert(element, 'bottom');
	                this.notifyObservers('itemadd');
                
	                var items = this._elements._items;
	                if (last) [].push.call(items, element.node);
                
	                return this;
	            },
            
	            /**
	             * <p>Removes the final item from the final page of the <tt>Paginator</tt>. If
	             * the final page subsequently contains no items, it is removed and a
	             * <tt>pagedestroy</tt> event is fired. The <tt>n</tt> parameter is for internal
	             * use only, for when items need to be moved across page boundaries by
	             * <tt>shift</tt>/<tt>unshift</tt> operations.</p>
	             * @param {Number} n
	             * @returns {DomCollection}
	             */
	            pop: function(n) {
	                n = (n === undefined) ? this._numPages - 1 : n;
	                var last = (n === this._numPages - 1),
	                    page = this._elements._pages[n],
	                    item;
                
	                if (!page) return;
                
	                item = Ojay(page.children().toArray().pop());
                
	                this.notifyObservers('itemremove');
	                if (!last) return item.remove();
                
	                this._elements._items = this._elements._items.filter(function(member) {
	                    return member.node !== item.node;
	                });
	                if (last) this._checkPages();
                
	                return item.remove();
	            },
            
	            /**
	             * <p>Removes the first item from the first page of the <tt>Paginator</tt>. If
	             * the final page subsequently contains no items, it is removed and a
	             * <tt>pagedestroy</tt> event is fired. The <tt>n</tt> parameter is for internal
	             * use only, for when items need to be moved across page boundaries by
	             * <tt>shift</tt>/<tt>unshift</tt> operations.</p>
	             * @param {Number} n
	             * @returns {DomCollection}
	             */
	            shift: function(n) {
	                n = (n === undefined) ? 0 : n;
	                var first = (n === 0),
	                    page = this._elements._pages[n],
	                    item;
                
	                if (!page) return;
                
	                item = page.children().at(0);
                
	                this.notifyObservers('itemremove');
	                if (!first) return item.remove();
                
	                for (var i = 1; i < this._numPages; i++)
	                    this.push(this.shift(i), i-1);
                
	                this._elements._items = this._elements._items.filter(function(member) {
	                    return member.node !== item.node;
	                });
	                this._checkPages();
                
	                return item.remove();
	            },
            
	            /**
	             * <p>Pushes a new element onto the start of the list of elements contained in the
	             * <tt>Paginator</tt>, creating a new page and firing the <tt>pagecreate</tt>
	             * event if necessary. The <tt>n</tt> parameter is for internal use only, for when
	             * items need to be moved across page boundaries by <tt>shift</tt>/<tt>unshift</tt>
	             * operations.</p>
	             * @param {HTMLElement} element
	             * @param {Number} n
	             * @returns {Paginator}
	             */
	            unshift: function(element, n) {
	                if (typeof n == 'object' && n.animate) return this._animatedUnshift(element);
                
	                n = (n === undefined) ? 0 : n;
	                var first = (n === 0);
	                if (first) this._checkPages();
                
	                element = Ojay(element).setStyle({margin: '0 0 0 0'});
	                var page = this._elements._pages[n];
                
	                page.insert(element, 'top');
	                this.notifyObservers('itemadd');
	                if (!first) return this;
                
	                for (var i = 1; i < this._numPages; i++)
	                    this.unshift(this.pop(i-1), i);
                
	                var items = this._elements._items;
	                [].unshift.call(items, element.node);
                
	                return this;
	            },
            
	            /**
	             * @returns {MethodChain}
	             */
	            _animatedUnshift: function(element) {
	                if ((this._options.direction == 'vertical' && this._itemsPerRow > 1) ||
	                    (this._options.direction == 'horizontal' && this._itemsPerCol > 1))
	                    throw new Error('Cannot perform animated push/unshift ' +
	                                    'onto a Paginator with more than one ' +
	                                    'column and row');
                
	                var item = Ojay(element).setStyle({opacity: 0});
                
	                var current = this.getCurrentOffset(),
                    
	                    nItems  = (this._options.direction == 'vertical') ?
	                              this._itemsPerCol : this._itemsPerRow,
                    
	                    offset  = current - this.getTotalOffset() /
	                              (nItems * (this.getPages() - 1));
                
	                return this.setScroll(offset, {animate: true, _scrollTime: this._options.pushSlide})
	                     .unshift(item)
	                     .setScroll(current)
	                     ._(item).animate({opacity: {to: 1}}, this._options.pushFade)
	                     ._(this);
	            },
            
	            /**
	             * <p>Used by the <tt>push</tt>, <tt>pop</tt>, <tt>shift</tt> and <tt>unshift</tt>
	             * operations to decide whether pages need to be created or destroyed.</p>
	             */
	            _checkPages: function() {
	                var items   = this._elements._items.length,
	                    pages   = this._numPages,
	                    perPage = this._itemsPerPage,
	                    total   = pages * perPage;
                
	                if (items == total) this._createPage();
	                if (items == total - perPage) this._destroyPage();
	            },
            
	            /**
	             * <p>Adds a new page at the end of the <tt>Paginator</tt>, firing the
	             * <tt>pagecreate</tt> and <tt>scroll</tt> events.</p>
	             */
	            _createPage: function() {
	                var region = this.getRegion(),
	                    page = this.klass.makePageElement(region.getWidth(), region.getHeight());
	                this._elements._subject.insert(page, 'bottom');
	                this._elements._pages.push(page);
                
	                this._numPages += 1;
	                var offset = (this._currentPage - 1) / (this._numPages - 1);
	                this.notifyObservers('pagecreate');
	                this.notifyObservers('scroll', offset, this.getTotalOffset());
	            },
            
	            /**
	             * <p>Removes the final page of the <tt>Paginator</tt>, firing the
	             * <tt>pagedestroy</tt>, <tt>scroll</tt> and (if needed) the
	             * <tt>pagechange</tt> events.
	             */
	            _destroyPage: function() {
	                this._elements._pages.pop().remove();
	                if (this._currentPage == this._numPages) {
	                    this._currentPage -= 1;
	                    this.notifyObservers('pagechange', this._currentPage);
	                }
	                this._numPages -= 1;
	                var offset = (this._currentPage - 1) / (this._numPages - 1);
	                if (offset == 1) this.setScroll(1, {animate: true, silent: true});
	                this.notifyObservers('pagedestroy');
	                this.notifyObservers('scroll', offset, this.getTotalOffset());
	            }
	        }
	    }
	});


	/**
	 * <p>The <tt>AjaxPaginator</tt> class extends the <tt>Paginator</tt> with functionality that
	 * allows you to load content for the pages from the server using Ajax. Content is lazy-loaded,
	 * which is to say that each page is not loaded until the user selects to view that page.</p>
	 * @constructor
	 * @class AjaxPaginator
	 */
	Ojay.AjaxPaginator = new JS.Class('Ojay.AjaxPaginator', Ojay.Paginator, /** @scope Ojay.AjaxPaginator.prototype */{
	    /**
	     * <p><tt>AjaxPaginator</tt> takes the same initialization data as <tt>Paginator</tt>, but
	     * with one extra required option: <tt>urls</tt>. This should be an array of URLs that
	     * the paginator will pull content from.</p>
	     * @param {String|HTMLElement|DomCollection} subject
	     * @param {Object} options
	     */
	    initialize: function(subject, options) {
	        this.callSuper();
	        this._options.urls = this._options.urls.map(function(url) {
	            return {_url: url, _loaded: false};
	        });
	    },
    
	    /**
	     * <p>Returns an Ojay collection wrapping the child elements of the subject.</p>
	     * @returns {DomCollection}
	     */
	    getItems: function() {
	        var elements = this._elements;
	        if (elements._items) return elements._items;
	        if (!elements._subject) return undefined;
	        var urls = this._options.urls;
	        if (!urls.length) return undefined;
	        urls.length.times(function(i) {
	            var item = Ojay( Ojay.HTML.div({className: this.klass.ITEM_CLASS}) );
	            elements._subject.insert(item.node, 'bottom');
	        }, this);
	        var items = this.callSuper();
	        items.fitToRegion(this.getRegion());
	        return items;
	    },
    
	    /**
	     * <p>Returns <tt>true</tt> iff the given page has its content loaded.</p>
	     * @param {Number} page
	     * @returns {Boolean}
	     */
	    pageLoaded: function(page) {
	        return !!(this._options.urls[page - 1]||{})._loaded;
	    },
    
	    /**
	     * <p>Tells the <tt>AjaxPaginator</tt> to load the content for the given page, if
	     * the content is not already loaded. Fires <tt>pagerequest</tt> and
	     * <tt>pageload</tt> events.</p>
	     * @param {Number} page
	     * @param {Function} callback
	     * @param {Object} scope
	     * @returns {AjaxPaginator}
	     */
	    loadPage: function(page, callback, scope) {
	        if (this.pageLoaded(page) || this.inState('CREATED')) return this;
	        var url = this._options.urls[page - 1], self = this;
	        this.notifyObservers('pagerequest', url._url);
	        Ojay.HTTP.GET(url._url, {}, {
	            onSuccess: function(response) {
	                response.insertInto(self._elements._items.at(page - 1));
	                url._loaded = true;
	                self.notifyObservers('pageload', url._url, response);
	                if (typeof callback == 'function') callback.call(scope || null);
	            }
	        });
	        return this;
	    },
    
	    states: {
	        READY: {
	            /**
	             * <p>Handles request to <tt>changeState()</tt>.</p>
	             * @param {Number} page
	             */
	            _handleSetPage: function(page) {
	                var n = this._options.urls.length;
	                if (page > n) page -= n;
	                if (page < 1) page += n;
                
	                if (this.pageLoaded(page)) return this.callSuper();
                
	                var _super = this.method('callSuper');
	                this.setState('REQUESTING');
	                this.loadPage(page, function() {
	                    this.setState('READY');
	                    _super();
	                }, this);
	            }
	        },
        
	        REQUESTING: {}
	    }
	});


	/**
	 * <p>The <tt>Paginator.Controls</tt> class implements a default UI for <tt>Paginator</tt>
	 * instances, which includes previous/next links, individual page links, and event listeners
	 * that add class names to the elements in the UI in response to state changes in the
	 * observed paginator object.</p>
	 * @constructor
	 * @class Paginator.Controls
	 */
	Ojay.Paginator.extend(/** @scope Ojay.Paginator */{
	    Controls: new JS.Class('Ojay.Paginator.Controls', /** @scope Ojay.Paginator.Controls.prototype */{
	        extend: /** @scope Ojay.Paginator.Controls */{
	            CONTAINER_CLASS:    'paginator-controls',
	            PREVIOUS_CLASS:     'previous',
	            NEXT_CLASS:         'next',
	            PAGE_LINKS_CLASS:   'pages'
	        },
        
	        /**
	         * <p>To initialize a <tt>Paginator.Controls</tt> instance, pass in the <tt>Paginator</tt>
	         * to which you want the generated UI elements to apply.</p>
	         * @param {Paginator}
	         */
	        initialize: function(paginator) {
	            this._paginator = paginator;
	            this._elements = {};
	            this._paginator.on('pagecreate')._(this)._addPage();
	            this._paginator.on('pagedestroy')._(this)._removePage();
	        },
        
	        /**
	         * <p>Returns the collection of HTML elements used to implement the UI. When the
	         * elements are first generated, all required event handlers (both DOM and
	         * Observable-based) are set up.</p>
	         * @returns {DomCollection}
	         */
	        getHTML: function() {
	            if (this._paginator.inState('CREATED')) return null;
	            var elements = this._elements, klass = this.klass, paginator = this._paginator;
	            if (elements._container) return elements._container;
	            var self = this;
            
	            elements._container = Ojay( Ojay.HTML.div(
	                {className: klass.CONTAINER_CLASS}, function(HTML) {
            
	                // Previous button - decrements page
	                elements._previous = Ojay( HTML.div(
	                        {className: klass.PREVIOUS_CLASS},
	                        'Previous') );
                
	                // Page buttons - skip to individual pages
	                elements._pageLinks = Ojay( HTML.div(
	                    {className: klass.PAGE_LINKS_CLASS}, function(HTML) {
	                    elements._pages = [];
	                    paginator.getPages().times(function(page) {
	                        var span = elements._pages[page] = self._makeLink(page+1);
	                        HTML.concat(span.node);
	                    });
	                }) );
                
	                // Next button - increments page
	                elements._next = Ojay( HTML.div(
	                        {className: klass.NEXT_CLASS},
	                        'Next') );
	            }) );
            
	            elements._previous.on('click')._(paginator).decrementPage();
	            elements._next.on('click')._(paginator).incrementPage();
            
	            // Delegate page click events to the container
	            elements._pageLinks.on('click', Ojay.delegateEvent({
	                span: function(element, evnt) {
	                    paginator.setPage(element.node.innerHTML);
	                }
	            }));
            
	            // Add hover states to previous and next buttons
	            var buttons = [elements._previous, elements._next];
	            buttons.forEach(it().on('mouseover').addClass('hovered'));
	            buttons.forEach(it().on('mouseout').removeClass('hovered'));
            
	            // Monitor page changes to highlight page links
	            paginator.on('pagechange', function(paginator, page) {
	                this._highlightPage(page);
	                buttons.forEach(it().removeClass('disabled'));
	            }, this);
	            var page = paginator.getCurrentPage();
	            this._highlightPage(page);
            
	            // Disable previous and next buttons at the ends of the run
	            if (!paginator.isLooped()) {
	                paginator.on('firstpage')._(elements._previous).addClass('disabled');
	                paginator.on('lastpage')._(elements._next).addClass('disabled');
	                if (page == 1) elements._previous.addClass('disabled');
	                if (page == paginator.getPages()) elements._next.addClass('disabled');
	            }
            
	            elements._container.addClass(paginator.getDirection());
	            return elements._container;
	        },
        
	        /**
	         * <p>Creates and returns an element to use as a numbered page link.</p>
	         * @param {Number} page
	         * @returns {DomCollection}
	         */
	        _makeLink: function(page) {
	            var link = Ojay( Ojay.HTML.span(String(page)) );
	            link.on('mouseover').addClass('hovered');
	            link.on('mouseout').removeClass('hovered');
	            return link;
	        },
        
	        /**
	         * <p>Responds to the <tt>pagecreate</tt> event on the associated <tt>Paginator</tt>
	         * instance by adding a new page link to the list.</p>
	         */
	        _addPage: function() {
	            var link = this._makeLink(this._paginator.getPages());
	            this._elements._pages.push(link);
	            this._elements._pageLinks.insert(link, 'bottom');
	            this._elements._next.removeClass('disabled');
	        },
        
	        /**
	         * <p>Responds to the <tt>pagedestroy</tt> event on the associated <tt>Paginator</tt>
	         * instance removing the final page link from the list.</p>
	         */
	        _removePage: function() {
	            this._elements._pages.pop().remove();
	            var pager = this._paginator;
	            if (pager.isLooped()) return;
	            if (pager.getCurrentPage() == pager.getPages())
	                this._elements._next.addClass('disabled');
	        },
        
	        /**
	         * <p>Adds the class 'selected' to the current page number.</p>
	         * @param {Number}
	         */
	        _highlightPage: function(page) {
	            var page = this._elements._pages[page - 1];
	            this._elements._pages.forEach({removeClass: 'selected'});
	            if (page) page.addClass('selected');
	        },
        
	        /**
	         * <p>Returns a reference to the 'previous' button.</p>
	         * @returns {DomCollection}
	         */
	        getPreviousButton: function() {
	            if (this._paginator.inState('CREATED')) return null;
	            return this._elements._previous;
	        },
        
	        /**
	         * <p>Returns a reference to the 'next' button.</p>
	         * @returns {DomCollection}
	         */
	        getNextButton: function() {
	            if (this._paginator.inState('CREATED')) return null;
	            return this._elements._next;
	        },
        
	        /**
	         * <p>Returns a reference to the collection of page number links.</p>
	         * @returns {DomCollection}
	         */
	        getPageButtons: function() {
	            if (this._paginator.inState('CREATED')) return null;
	            return this._elements._pageLinks;
	        }
	    })
	});


	/**
	 * <p>The <tt>FilmStrip</tt> class is similar to the <tt>Paginator</tt> class, in that it
	 * is designed to shrink a block of content into a smaller scrollable area. The difference
	 * is that where <tt>Paginator</tt> affords a grid of homogenously sized elements, the
	 * <tt>FilmStrip</tt> class is used for a single row or column of elements of different
	 * widths (for a row) or heights (for a column). A good example would be a strip of images
	 * all with the same height, but different widths (hence the name).</p>
	 *
	 * <p>The starting point for creating a <tt>FilmStrip</tt> is the same as for <tt>Paginator</tt>,
	 * for example a list of images:</p>
	 *
	 * <pre><code>    &lt;div id="gallery"&gt;
	 *         &lt;div class="item"&gt;&lt;img src="01.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="02.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="03.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="04.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="05.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="06.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="07.jpg" /&gt;&lt;/div&gt;
	 *         &lt;div class="item"&gt;&lt;img src="08.jpg" /&gt;&lt;/div&gt;
	 *     &lt;/div&gt;</code></pre>
	 *
	 * <p>After applying a <tt>FilmStrip</tt> to this markup, it is wrapped in some elements
	 * to facilitate, scrolling, leaving us with:</p>
	 *
	 * <pre><code>    &lt;div class="filmstrip"&gt;
	 *         &lt;div id="gallery"&gt;
	 *             &lt;div class="item"&gt;&lt;img src="01.jpg" /&gt;&lt;/div&gt;
	 *             &lt;div class="item"&gt;&lt;img src="02.jpg" /&gt;&lt;/div&gt;
	 *             &lt;div class="item"&gt;&lt;img src="03.jpg" /&gt;&lt;/div&gt;
	 *             &lt;div class="item"&gt;&lt;img src="04.jpg" /&gt;&lt;/div&gt;
	 *             &lt;div class="item"&gt;&lt;img src="05.jpg" /&gt;&lt;/div&gt;
	 *             &lt;div class="item"&gt;&lt;img src="06.jpg" /&gt;&lt;/div&gt;
	 *             &lt;div class="item"&gt;&lt;img src="07.jpg" /&gt;&lt;/div&gt;
	 *             &lt;div class="item"&gt;&lt;img src="08.jpg" /&gt;&lt;/div&gt;
	 *         &lt;/div&gt;
	 *     &tl;/div&gt;</code></pre>
	 *
	 * <p>Note that <tt>FilmStrip</tt> does not split the content into pages, it merely wraps
	 * it in a an element to allow clipping and scrolling. The 'pages' are simply the
	 * individual items; calling <tt>setPage()</tt> centers the given item within the
	 * visible portion of the strip.</p>
	 *
	 * <p>Some methods in this class may have slightly odd names; this is because I
	 * want the API to be reasonably close to that for <tt>Paginator</tt> so they can
	 * be easily interchanged.</p>
	 *
	 * @constructor
	 * @class FilmStrip
	 */
	Ojay.FilmStrip = new JS.Class('Ojay.FilmStrip', /** @scope Ojay.FilmStrip.prototype */{
	    include: Ojay.Paginatable,
    
	    extend: /** @scope Ojay.FilmStrip */{
	        CONTAINER_CLASS:    'filmstrip',
	        PAGE_CLASS:         'item',
	        SCROLL_TIME:        0.5,
	        DIRECTION:          'horizontal',
	        EASING:             'easeBoth',
        
	        /**
	         * <p>A small data class for representing items. When an item is clicked,
	         * its parent <tt>FilmStrip</tt> focuses on it.</p>
	         * @constructor
	         * @class Ojay.FilmStrip.Item
	         */
	        Item: new JS.Class({
	            /**
	             * @param {FilmStrip} strip
	             * @param {DomCollection} element
	             */
	            initialize: function(strip, element) {
	                this._element = element;
	                var region    = element.getRegion();
	                this._width   = region.getWidth();
	                this._height  = region.getHeight();
                
	                this._strip = strip;
	                element.on('click')._(strip).focusItem(this);
	            },
            
	            /**
	             * @returns {Number}
	             */
	            getWidth: function() {
	                return this._width;
	            },
            
	            /**
	             * @returns {Number}
	             */
	            getHeight: function() {
	                return this._height;
	            }
	        })
	    },
    
	    /**
	     * <p>To initialize, the <tt>FilmStrip</tt> instance needs a CSS selector and some configuration
	     * options. Available options are:</p>
	     *
	     * <ul>
	     *      <li><tt>width</tt> - the width as a string, in any units, e.g. '512px'.</li>
	     *      <li><tt>height</tt> - the height as a string, in any units, e.g. '512px'.</li>
	     *      <li><tt>scrollTime</tt> - the duration of the scoll effect in seconds.</li>
	     *      <li><tt>easing</tt> - sets the name of the easing effect to use.</li>
	     *      <li><tt>direction</tt> - 'horizontal' or 'vertical', sets scroll direction.</li>
	     * </ul>
	     *
	     * @param {String|HTMLElement|DomCollection} subject
	     * @param {Object} options
	     */
	    initialize: function(subject, options) {
	        this._selector = subject;
	        this._elements = {};
        
	        options = this._options = options || {};
	        options.scrollTime = options.scrollTime || this.klass.SCROLL_TIME;
	        options.direction  = options.direction  || this.klass.DIRECTION;
	        options.easing     = options.easing     || this.klass.EASING;
	        this.setState('CREATED');
	    },
    
	    /**
	     * <p>Returns an Ojay collection wrapping all the HTML used by the filmstrip.</p>
	     * @returns {DomCollection}
	     */
	    getHTML: function() {
	        var elements = this._elements, options = this._options;
	        if (elements._container) return elements._container;
	        var container = Ojay( Ojay.HTML.div({className: this.klass.CONTAINER_CLASS}) );
	        container.addClass(this._options.direction);
        
	        var dimensions = this.getDimensions(),
	            vertical   = (this.getDirection() == 'vertical'),
	            width      = vertical ? dimensions.width + 'px' : options.width,
	            height     = vertical ? options.height : dimensions.height + 'px';
        
	        container.setStyle({
	            width:      width,
	            height:     height,
	            overflow:   'hidden',
	            padding:    '0 0 0 0',
	            border:     'none',
	            position:   'relative'
	        });
	        return elements._container = container;
	    },
    
	    /**
	     * @returns {Number}
	     */
	    getTotalOffset: function() {
	        var region = this.getRegion(),
	            dims   = this.getDimensions();
        
	        return this.getDirection() == 'vertical'
	            ? dims.height - region.getHeight()
	            : dims.width  - region.getWidth();
	    },
    
	    /**
	     * @returns {Array}
	     */
	    getScrollLimits: function() {
	        return (this._options.overshoot === false)
	                ? [0, this.getTotalOffset()]
	                : [this._offsetForPage(1), this._offsetForPage(this._numPages)];
	    },
    
	    /**
	     * <p>Returns an Ojay collection wrapping the child elements of the subject.</p>
	     * @returns {DomCollection}
	     */
	    getItems: function() {
	        if (this._items) return this._items;
        
	        return this._items = this._elements._subject.children().map(function(child) {
	            return new this.klass.Item(this, child);
	        }, this);
	    },
    
	    /**
	     * <p>Returns the number of child elements the <tt>FilmStrip</tt> has.</p>
	     * @returns {Number}
	     */
	    getPages: function() {
	        return this._numPages = this.getItems().length;
	    },
    
	    /**
	     * <p>Returns the page corresponding to the given absolute offset.</p>
	     * @returns {Number}
	     */
	    _pageFromOffset: function(offset) {
	        var vertical = (this.getDirection() == 'vertical'),
	            method   = vertical ? 'getHeight' : 'getWidth',
	            center   = this.getRegion()[method]() / 2,
	            i        = 1,
	            page     = null;
        
	        this._getEdges().reduce(function(x,y) {
	            if (page !== null) return;
	            if (x - offset <= center && y - offset >= center) page = i;
	            i += 1;
	            return y;
	        });
	        return page;
	    },
    
	    /**
	     * <p>Returns the scroll offset fo the given page, ignoring overshoot clipping.</p>
	     * @param {Number} page
	     * @returns {Number}
	     */
	    _offsetForPage: function(page) {
	        var vertical = (this.getDirection() == 'vertical'),
	            method   = vertical ? 'getHeight' : 'getWidth',
	            center   = this.getRegion()[method]() / 2;
       
	       return this._getCenters()[page - 1] - center;;
	    },
    
	    /**
	     * <p>Returns an object with <tt>width</tt> and <tt>height</tt> attributes
	     * describing the size of the content contained in the filmstrip.</p>
	     * @returns {Object}
	     */
	    getDimensions: function() {
	        var vertical = (this.getDirection() == 'vertical'),
	            width    = 0,
	            height   = 0;
        
	        this.getItems().forEach(function(item) {
	            if (vertical) {
	                width   = Math.max(width, item.getWidth());
	                height += item.getHeight();
	            } else {
	                width  += item.getWidth();
	                height  = Math.max(height, item.getHeight());
	            }
	        });
        
	        return {width: width, height: height};
	    },
    
	    /**
	     * <p>Returns an array containing the pixel offsets of the edges between adjacent
	     * child elements, including the leading and trailing edge of the whole set.</p>
	     * @returns {Array}
	     */
	    _getEdges: function() {
	        if (this._edges) return this._edges.slice();
        
	        var vertical = (this.getDirection() == 'vertical'),
	            method   = vertical ? 'getHeight' : 'getWidth',
	            edges    = [0];
        
	        this.getItems().forEach(function(item) {
	            var size     = item[method](),
	                previous = edges[edges.length-1];
	            edges.push(previous + size);
	        });
        
	        return (this._edges = edges).slice();
	    },
    
	    /**
	     * <p>Returns an array of the pixel offsets of the centers of the child elements.</p>
	     * @returns {Array}
	     */
	    _getCenters: function() {
	        var centers = [];
	        this._getEdges().reduce(function(x,y) {
	            centers.push((x + y) / 2);
	            return y;
	        });
	        return centers;
	    },
    
	    states: {
	        /**
	         * <p>The <tt>FilmStrip</tt> is in the CREATED state when it has been instantiated but
	         * none of its DOM interactions have taken place. This attachment is deferred to the
	         * <tt>setup()</tt> call so that object can be history-managed before its UI is set up.</p>
	         */
	        CREATED: {
	            /**
	             * <p>Sets up all the DOM changes the <tt>FilmStrip</tt> needs. If you want to history
	             * manage the object, make sure you set up history management before calling this method.
	             * Moves the object to the READY state if successful.</p>
	             * @returns {FilmStrip}
	             */
	            setup: function() {
	                var subject = this._elements._subject = Ojay(this._selector).at(0);
	                if (!subject.node) return this;
                
	                var container = this.getHTML();
	                subject.insert(container.node, 'after');
	                container.insert(subject.node);
	                subject.setStyle({padding: '0 0 0 0', border: 'none', position: 'absolute', left: 0, top: 0});
                
	                var dims  = this.getDimensions();
                
	                var style = (this._options.direction == 'vertical')
	                        ? { width: dims.width + 'px', height: (dims.height + 1000) + 'px' }
	                        : { width: (dims.width + 1000) + 'px', height: dims.height + 'px' };
                
	                subject.setStyle(style);
                
	                var state = this.getInitialState();
	                this.setState('READY');
	                if (this._currentPage === undefined) this._currentPage = state.page;
	                this.wait(0.001)._handleSetPage(this._currentPage);
                
	                return this;
	            }
	        },
        
	        /**
	         * <p>The <tt>FilmStrip</tt> is in the READY state when all its DOM behaviour has been
	         * set up and it is not in the process of scrolling.</p>
	         */
	        READY: {
	            /**
	             * <p>Handles request to <tt>changeState()</tt>.</p>
	             * @param {Number} page
	             * @param {Function} callback
	             * @param {Object} scope
	             */
	            _handleSetPage: function(page, callback, scope) {
	                var offset = this._offsetForPage(page);
	                this.setScroll(offset, {animate: true}, callback, scope);
	            },
            
	            /**
	             * <p>Increments the current page by one, firing a <tt>pagechange</tt> event.</p>
	             * @returns {FilmStrip}
	             */
	            incrementPage: function() {
	                return this.setPage(this._currentPage + 1);
	            },
            
	            /**
	             * <p>Decrements the current page by one, firing a <tt>pagechange</tt> event.</p>
	             * @returns {FilmStrip}
	             */
	            decrementPage: function() {
	                return this.setPage(this._currentPage - 1);
	            },
            
	            // TODO Paginator#snapToPage
            
	            /**
	             * <p>For filmstrips, this accepts a <tt>FilmStrip.Item</tt> and centers the
	             * view on that item. Use to handle clicks on individual items. If called with
	             * a number, simply centers on that numbered page.</p>
	             * @param {FilmStrip.Item|Number} item
	             * @returns {FilmStrip}
	             */
	            focusItem: function(item) {
	                if (typeof item !== 'number') item = this._items.indexOf(item) + 1;
	                this.setPage(item);
	                return this;
	            }
	        }
	    }
	});
});
