﻿var _frameRegex = /\bf(rame)?(\d+)\b/;
var _frameStartRegex = /\bframeStart|fs(\d+)\b/;
var _frameEndRegex = /\bframeEnd|fe(\d+)\b/;

if( ! Array.prototype.map )
{
    Array.prototype.map = function( fn )
    {
        var results = new Array( this.length );
        
        for( var i=0; i<this.length; i++ )
        {
            results[ i ] = fn( this[ i ] );
        }
        
        return results;
    }
}

/*
 * Creates a Movie containing jFrameSets
 */
function jMovie( options )
{
    var me = this;
    
//  PROPERTIES
    if( ! options ) options = { };
    if( ! options.buttons ) options.buttons = { };
    if( ! options.events ) options.events = { };
    if( ! options.frameSets ) options.frameSets = [ ];    
    
    var _last = 0;
    var _index = null;  // required so go( 0 ) can work
    
    var _timer = null;
    var _rate = 1000;
    
    var _loadedImages = 0;
    var _totalImages = 0;

    this.index = function( )
    {
        return _index;
    }
    
    this.last = function( )
    {
        return _last;
    }
    
//  FRAMESETS
    this.frameSets = function( index )
    {
        if( index == null ) return options.frameSets;
        
        if( index < 0 || index >= options.frameSets.length ) return null;
        
        return options.frameSets[ index ];
    }
    
//  NAVIGATE    
    this.cancel = function( )
    {
        if( options.events.onCancel )
            options.events.onCancel( );    
    }
    
    this.go = function( index, direction )
    {
        if( index == _index ) return false;
     
        if( ! _index ) _index = 0;
     
        // check the frames changing
        for( var i=0; i<options.frameSets.length; i++ )
        {
            var frame = options.frameSets[ i ];
            
            if(
                options.frameSets[ i ]._onPreFrameChange &&
                options.frameSets[ i ]._onPreFrameChange( frame, _index, index )
            ) return false;
        }
     
        // change the index first
        _index = index;
     
        if( options.events.onFrameChange )
            options.events.onFrameChange( me, index );
     
        // change the frames
        for( var i=0; i<options.frameSets.length; i++ )           
            options.frameSets[ i ].go( index, direction );
     
        if( options.buttons.back )
        {
            if( index > 1 )
                options.buttons.back.removeAttribute( 'disabled' );
            else
                options.buttons.back.setAttribute( 'disabled', 'disabled' );
        }
     
        if( options.buttons.next )
        {
            var nextButtonLabel = 'Next';
                        
            if( index == me.last( ) )
                nextButtonLabel= 'Finish';
            else if( index == 0 )
                nextButtonLabel = 'Start';

            if( options.buttons.next.nodeName.toLowerCase( ) == 'input' )
            {
                options.buttons.next.setAttribute( 'value', nextButtonLabel );
            }
            else
            {
                var el = options.buttons.next.firstChild;
                
                while( el )
                {
                    if( el.nodeType == 3 )
                    {
                        el.nodeValue = nextButtonLabel;
                        break;
                    }
                
                    el = el.nextSibling;
                }
            }
                
            if( index <= me.last( ) )
                options.buttons.next.removeAttribute( 'disabled' );
            else
                options.buttons.next.setAttribute( 'disabled', 'disabled' );
        }

        if( index > _last && options.events.onFinish )
        {
            options.events.onFinish( );
            return false;
        }

        return index >= 0 && index <= _last;
    }
    
    this.reset = function( )
    {
        me.pause( );
        me.go( 0 );
    }
    
//  PLAY / PAUSE
    this.play = function( rate )
    {
        me.pause( );
        
        if( rate ) _rate = rate;
        
        me._timer = window.setInterval(
            _doPlay, _rate
        );
    }
    
    this.pause = function( )
    {
        if( me._timer )
            window.clearInterval( me._timer );
            
        me._timer = null;
    }
    
    var _doPlay = function( )
    {
        if( me.go( me.index( ) + 1 ) == false )
            me.pause( );
    }
    
    this.isPlaying = function( )
    {
        return me._timer != null;
    }
    
//  IMAGE PRELOAD    
    var _loadImages = function( )
    {
        var uniqueURLs = { };
    
        // find out what images need to be loaded
        for( var i=0; i<options.frameSets.length; i++ )
        {
            var imageEls = $( 'img', options.frameSets[ i ].container( ) );
            
            for( var j=0; j<imageEls.length; j++ )
            {
                uniqueURLs[ imageEls[ j ].src ] = true;
            }
        }
        
        // set the total number of unique images
        // this has to be done first to prevent race conditions
        for( var k in uniqueURLs )
            _totalImages++;
        
        // create the in-memory images
        for( var k in uniqueURLs )
        {
            var img = new Image( );
            $( img ).bind( 'load', null, _onImageLoad );
            img.src = k;
        }
    }
    
    var _onImageLoad = function( )
    {
        _loadedImages++;
     
        options.events.onLoadChange(
            me, _loadedImages, _totalImages
        );
    }
    
//  SET UP
    var _wireButtons = function( )
    {
        var ob = options.buttons;

        if( ob.back )
        {
            ob.back.setAttribute( 'disabled', 'disabled' );
            $( ob.back ).click( function( ) { me.go( me.index( ) - 1, 'back' ); } );
        }
            
        if( ob.next && ! me.last( ) )
        {
            ob.next.setAttribute( 'disabled', 'disabled' );
        }
        else if( ob.next )
        {
            $( ob.next ).click( function( ) { me.go( me.index( ) + 1, 'forward' ); } );
        }
            
        if( ob.cancel && ! ( options.events.onCancel ) )
            ob.cancel.setAttribute( 'disabled', 'disabled' );
        else if( ob.cancel )
            $( ob.cancel ).click( me.cancel );
                
            
        if( ob.reset )
            $( ob.reset ).click( me.reset );
    }
    
    for( var i=0; i<options.frameSets.length; i++ )
    {
        options.frameSets[ i ]._movie = this;
        
        var last = options.frameSets[ i ].last( );
        
        if( last > _last ) _last = last;
    }
        
    if( options.events.onLoadChange )
    {
        _loadImages( );
        
        if( _totalImages == 0 )
            options.events.onLoadChange( me, 0, 0 );
    }
    
    _wireButtons( );
        
    this.go( 0 );
    
    return this;
}

/*
 * A set of frames acting as a layer in the movie
 */
function jFrameSet( options )
{
    var me = this;
    
//  PROPERTIES
    if( ! options ) options = { };
    var _frames = [ ];
    
    this._onPreFrameChange = options.onPreFrameChange;
    
    this.container = function( )
    {
        return options.container;
    }
    
    this.last = function( )
    {
        return _last;
    }
    
//  NAVIGATE
    this.go = function( index, direction )
    {
        if( ! direction ) direction = 'jump';
    
        for( var i=0; i<_frames.length; i++ )
        {
            if( ! options.onPreFrameRender || options.onPreFrameRender( me, index ) )
            {
                _frames[ i ].element.style.display =
                    _isWithin( _frames[ i ], index ) ? 'block' : 'none';
            }
        }
        
        if( options.onFrameChange )
            options.onFrameChange( me, index, direction );
    }
    
//  FRAME VISIBILITY
    this.visibleFrames = function( )
    {
        var visibleFrames = [ ];
            
        for( var i=0; i<_frames.length; i++ )
        {
            var frame = _frames[ i ];
            
            if( _isWithin( frame, me._movie.index( ) ) )
                visibleFrames[ visibleFrames.length ] = frame;                
        }
        
        return visibleFrames;
    }
        
    this.invisibleFrames = function( )
    {
        var invisibleFrames = [ ];
            
        for( var i=0; i<_frames.length; i++ )
        {
            var frame = _frames[ i ];
            
            if( ! _isWithin( frame, me._movie.index( ) ) )
                invisibleFrames[ invisibleFrames.length ] = frame;                
        }
        
        return invisibleFrames;
    }
    
    this.visibleElements = function( )
    {
        return me.visibleFrames( ).map( function( f ) { return f.element; } );
    }
    
    this.invisibleElements = function( )
    {
        return me.invisibleFrames( ).map( function( f ) { return f.element; } );
    }
    
//  SETUP AND HELPERS
    this._readElements = function( )
    {
        if( ! options.container ) return 0;
        
        var children = options.container.childNodes;
        
        var currentFrameIndex = 0;
        
        var lastAutoFrame = null;
        
        for( var i=0; i<children.length; i++ )
        {
            // ignore any non-element nodes
            if( children[ i ].nodeType != 1 ) continue;
            
            var className = children[ i ].className || "";

            var frame =
            {
                element: children[ i ]
            };
            
            // auto frame
            if( ! className )
            {            
                lastAutoFrame = frame;
                
                currentFrameIndex++;
                
                frame.start = currentFrameIndex;
                frame.end = currentFrameIndex;
            }    
            
            // single key frame
            else if( _frameRegex.exec( className ) )
            {                    
                frame.start = parseInt( RegExp.$2 );
                frame.end   = parseInt( RegExp.$2 );
                
                currentFrameIndex = frame.end;
                
                if( lastAutoFrame )
                {
                    lastAutoFrame.end = frame.start - 1;
                    lastAutoFrame = null;
                }
            }
            
            // spanned key frame
            else
            {
                // specified start
                if( _frameStartRegex.exec( className ) )
                    frame.start = parseInt( RegExp.$1 );
                else
                    frame.start = ++currentFrameIndex;
                
                if( lastAutoFrame )
                {
                    lastAutoFrame.end = frame.start - 1;
                    lastAutoFrame = null;
                }
                
                // specified end
                if( _frameEndRegex.exec( className ) )
                {
                    frame.end = parseInt( RegExp.$1 );
                }
                else
                {
                    frame.end = frame.start
                    lastAutoFrame = frame;
                }
                
                currentFrameIndex = frame.end;
            }
            
            _frames[ _frames.length ] = frame;
        }
        
        if( lastAutoFrame )
            lastAutoFrame.end = currentFrameIndex;
            
        return currentFrameIndex;
    }

    var _isWithin = function( frame, index )
    {    
        return frame.start <= index && frame.end >= index;
    }
    
    var _last = this._readElements( );
    
    return this;
}