//
//  movideo.ui.videobar.js
//  movideo Video Bar jQuery Plugin
//
//  Created by Victor Nguyen (victor@igloo.com.au) on 2011-02-22.
//  Copyright 2010 movideo. All rights reserved.
//

(function($, undefined){

    /*
        VideoBar Constructor
        @param  $el (jQuery collection)     The element to initialise
        @param  options (Object)
    */
    var VideoBar = function($el, opts) {
        
        // private vars
        var self = this,
            $vb  = $.fn.videobar;   // caching plugin namespace
        
        // public vars
        this.app         = null;
        this.initialised = false;
        this.mouseover   = false;
        
        // TODO: do these need to be public?
        this.count         = 0;         // media item count
        this.currentPos    = null;
        this.stepPx        = null;
        this.stepIncrement = null;      // number of px to increment, factoring in o.stepSize
        this.pagePx        = null;
        this.maxTravel     = null;
        this.listPx        = null;
        
        // ===================
        // = Private Methods =
        // ===================
        
        /*
            Requests movideo session
        */
        function _requestSession(key, alias, api) {
          if (key != null && alias != null) {
			  MOVIDEO.init({
                    appAlias: alias,
					apiKey: key,
                    api: api,
                    authHandler: function() {
                         _handleApplication();
                    }
                });
            } else {
                _handleApplication();
            }
        }
        
        
        function _handleApplication() {
           
            var value,
                values = {
                    playlist: o.playlistId,
                    media: o.mediaId,
                    apiCall: o.apiCall
                };
            
            for (key in values) {
                value = values[key];
                if (value) {
                    movideoGet[key].apply(self,[value]);
                    break;
                }  
            }
        }
        
        /*
            Define movideo media getter functions
            Called with apply() to bind methods to constructor instance
            e.g. movideoGet.playlist.apply(self, [params]);
        */
        // TODO: combine handlers in one obj and exend all of these...
        var movideoGet = {
            
            playlist: function(id) {
                // console.log('movideoGet.playlist()', id);
                MOVIDEO.media.getPlaylist({
                    id: id,
                    media: true,
                    omit: true,
                    mediaLimit: o.mediaLimit,
                    handler: _handleMedia,
                    errorHandler: _handleHttpError
                });
            },
            
            media: function(id) {
                // console.log('movideoGet.media()', id);
                MOVIDEO.media.getMedia({
                    id: id,
                    omit: true,
                    mediaLimit: o.mediaLimit,
                    handler: _handleMedia,
                    errorHandler: _handleHttpError
                });
            },
            
            apiCall: function(endpoint) {
                MOVIDEO.media.callAPI({
                    endpoint: endpoint,
                    handler: _handleMedia,
                    errorHandler: _handleHttpError
                });
            }
            
        };
        
        /*
            Returns normalised media items as an array of media objects.
            Media data returned from the movideoGet methods can return an Array, an Object or a String
            depending on the number of media items in the set. This will normalise all that.
            @param data (Object)        Data returned from MOVIDEO.media getter methods
            @return mediaItems (Array)  Array of media items objects, [ {},{} ]
        */
        function _getMediaItems(data) {
            var mediaItems;
			self.data =data;
			
            // Search results data
            // list.media: {} or [] or ''
            if ('pager' in data) {
		//		console.log('+++ data is media list!');
                mediaItems = (data.pager.list[0].media !== undefined) ? data.pager.list[0].media : null;
	//			console.log(mediaItems);
            }
            
            // Playlist results data
            // data.mediaList.media: {} or [] or ''
            else if ('playlist' in data) {
      //         console.log('+++ data is playlist!');
                mediaItems = (data.playlist.mediaList.media !== undefined) ? data.playlist.mediaList.media : null;
	//			console.log(mediaItems);
            }
            
            // Media results data (MULTIPLE)
            // data.media: [] or ''
            else if ('media' in data) {
			//	console.log('+++ data is media !');
                // console.log('is many media items!');
                mediaItems = data.media;
            }
            
            // Media results data (SINGLE)
            // data: {} or ''
            else {
                // console.log('is a single media item!');
                mediaItems = data;
            }
            
            var type = _getType(mediaItems);
		     switch(type) {
                case 'Object':
                	return [mediaItems];
                case 'Array':
                    return mediaItems;
                default:
                	return null;
            }
        }
        
        /*
            Handle media
        */
        function _handleMedia(data) {
            var mediaItems = _getMediaItems(data),$listItems = $([]),$item;
            // TODO: throw here if !mediaItems
			if(!mediaItems){
				_hideLoading();
			//	console.log('noresults');
				_trigger('noresults', self);
					return;
				}
            
            // build media items
            for (var i=0,j=mediaItems.length; i<j; i++) {
                $item = self.buildMediaItem(mediaItems[i]);
                $listItems = self.$listItems = $listItems.add($item);
            };
            
            // sequential mode: append items...
            if (self.o.mode == 'sequential') {
                
                // append media items to list
                // single-list
                // self.$lists[0].append($listItems);
                
                // multiple lists
                // group items by pageSize in order to populate lists in the correct order
                var group = [];
                var groups = [group];
                var groupSize = self.o.pageSize;
                for (var i=0, j=0, n=$listItems.length; i<n; i++) {
                    var item = $listItems[i];
                    group[group.length] = item;
                    j++;
                    if (j == groupSize) {
                        j = 0;
                        group = [];
                        groups[groups.length] = group;
                    }
                }
                
                // console.log('_handleMedia, groups', groups);
                for (var i=0, n=groups.length, rows=self.o.rows; i<n; i++) {
                    var list = self.$lists[i % rows];
                    list.append(groups[i]);
                }
                
                // update all relevant size values
                self.count = $listItems.length;
                self.currentPos = 0;
                self.stepPx = (self.o.display == 'wide') ? $listItems.first().outerWidth() : $listItems.first().outerHeight();
                self.stepIncrement = ( self.stepPx * self.o.stepSize );
                self.pagePx = ( self.stepPx * self.o.pageSize );
                self.listPx = ( self.stepPx * self.count );
                self.maxTravel = -( self.listPx - self.pagePx );
                                
                // set width/height on list container
                if (self.o.display == 'wide')
                    $(self.$lists).each(function(index, value) { 
                        value.outerWidth( self.listPx );
                    });
                else
                    $(self.$lists).each(function(index, value) { 
                        value.outerHeight( self.listPx );
                    });
                
                _updateNav();
            }
            
            // random mode: append items...
            else if (self.o.mode == 'random') {
                
                (function _injectRandom() {
                    // if mouse is not over element...
                    if (!self.mouseover) {
                        // remove displayed media items
                        self.$list.children().detach();

                        var arr = [], index, $selected;
                        for (var i=0; i < self.o.pageSize; i++) {
                            // generate a random index
                            index = Math.floor(Math.random() * $listItems.length);

                            // keep generating randoms until result is not a previously generated one stored in arr
                            while ($.inArray(index, arr) != -1)
                                index = Math.floor(Math.random() * $listItems.length);

                            // store random index in array so we don't select it again...
                            arr.push(index);

                            // append and animate item in...
                            $selected = $listItems.eq(index).css({ opacity:0 });
                            self.$list.append($selected.show().delay(i*100).animate({ opacity:1 }, 300, function(){ if ($.browser.msie) this.style.removeAttribute('filter'); }));
                        };                        
                    }
                    
                    // set timout to call this function expression again, and assign the timeout to an instance var...
                    self.timeout = setTimeout(_injectRandom, self.o.randomDelay);
                })();
                
            }
            
            // hide loading state
            _hideLoading();
        }
        
        /*
            Builds and attaches interface
        */
        function _buildInterface() {
            // console.log('_buildInterface, building', o.nameSpace, o.display,'interface');
            var ns = o.nameSpace;
            
            // add container class to $el
            $el.addClass(ns+'-container '+ns+'-'+o.display);
            
            // build and append viewport div
            var $v = self.$viewport = $('<div>');
            $v.addClass(ns+'-viewport').appendTo($el);
            if (o.widthViewport)
                $v.css({ width:o.widthViewport });
            if (o.heightViewport)
                $v.css({ height:o.heightViewport });
            // $v = _applyStyle($v, 'widthViewport');
            // $v = _applyStyle($v, 'heightViewport');
            
            // build and append list container
            // multiple rows
            var $lists = self.$lists = [];
            for (var i=0; i < self.o.rows; i++) {
                var $l = $('<ul>');
                $l.addClass(ns+'-list').appendTo(self.$viewport);
                $lists.push($l);
            }
            
            // single row
            // var $l = self.$list = $('<ul>');
            // $l.addClass(ns+'-list').appendTo(self.$viewport);
            
            // apply positioning css according to display mode
            if (o.display == 'wide') {
                $(self.$lists).each(function(index, value) { 
                    value.css({ 'left': 0, 'top': index * parseInt(value.css('height')) }); 
                });
                if (o.widthList)
                    $(self.$lists).css({ width: o.widthList });
                if (o.heightList)
                    $(self.$lists).css({ height: o.heightList });
                // _applyStyle($l, 'heightViewport');
            }
            else {
                $(self.$lists).each(function(index, value) { 
                    value.css({ 'top': 0, 'left': index * parseInt(value.css('width')) });
                });
                if (o.widthList)
                    $(self.$lists).css({ width: o.widthList });
                if (o.heightList)
                    $(self.$lists).css({ height: o.heightList }); 
                // _applyStyle($l, 'heightViewport');
            }
            
            // build navigation if not random
            if (o.mode !== 'random' && o.showNav !== false) {
			    var prev = self.$navPrev = $('<a href="#" class="'+ ns +'-nav '+ ns +'-nav-prev" rel="prev">Previous</a>');
                var next = self.$navNext = $('<a href="#" class="'+ ns +'-nav '+ ns +'-nav-next" rel="next">Next</a>');
                prev.prependTo($el);
                next.appendTo($el);
                self.$nav = self.$navPrev.add(self.$navNext);
                if (o.heightViewport) {
                    prev.css('height', o.heightViewport);
                    next.css('height', o.heightViewport);
                }
            }
            
            // build drawer
            if (o.showDrawer) {
                var $d = self.$drawer = $('<div class="'+ ns +'-drawer"></div>');
                self.$drawerTitle = $('<p class="'+ o.nameSpace +'-drawer-title"></p>');
                self.$drawerDesc = $('<p class="'+ o.nameSpace +'-drawer-desc"></p>');
                var drawerStyle = { position:'absolute' };
                drawerStyle[o.drawerSide] = 0;
                switch(o.drawerSide) {
                    case 'top':
                    	drawerStyle.left = 0;
                    	break;
                    case 'bottom':
                    	drawerStyle.left = 0;
                    	break;
                    case 'left':
                    	drawerStyle.top = 0;
                    	break;
                    case 'bottom':
                    	drawerStyle.top = 0;
                    	break;
                }
                drawerStyle[o.drawerSide] = 0;
                $d
                    .css(drawerStyle)
                    .append(self.$drawerTitle, self.$drawerDesc)
                    .wrapInner('<div class="'+ ns +'-drawer-inner"></div>')
                    .hide()
                    .prependTo($el);

            }
            
            // set container dimensions
            if (o.widthContainer)
                $el.width(o.widthContainer);
            if (o.heightContainer)
                $el.height(o.heightContainer);

            
            // build loading mask
            self.$mask = $('<div class="'+ ns +'-loading"></div>')
                .css({
                    'width': o.widthLoading ? o.widthLoading : $el.width(),
                    'height': o.heightLoading ? o.heightLoading : $el.height(),
                    // 'width': '100%',
                    // 'height': '100%',
                    'position': 'absolute',
                    'top': '0',
                    'left': '0',
                    'z-index': '1000',
                    'opacity': 0.6
                })
                .hide();
            $el.append(self.$mask);
            // show loading mask
            _showLoading();
            $el[0].className = $el[0].className;
        }
        
        /*
            Attach event handlers to interface
        */
        function _attachEvents() {
            // navigation
			
				if(o.noresultsHandler!=null){
				$el.bind('videobarnoresults',o.noresultsHandler)
				}
			
            if (o.mode !== 'random' && o.showNav !== false)
                self.$nav.bind({ click:_handleNav });
            
            // drawer events
            if (o.showDrawer) {
                self.$list
                    .delegate('li.'+ o.nameSpace + '-item', 'mouseenter', function(e) {
                        _showDrawer($(this).data('movideoMediaItem'));
                    })
                    .delegate('li.'+ o.nameSpace + '-item', 'mouseleave', function(e) {
                        _hideDrawer();
                    });
            }
            
            // bind mouseover and mouseout events on $list
            $el.bind({
                mouseenter: function() { self.mouseover = true; },
                mouseleave: function() { self.mouseover = false; }
            });
            
            // click events on media item links
            $(self.$lists).each(function(index, value) {
                value.delegate('a.'+ o.nameSpace +'-item-link', 'click', function(e) {
                    _trigger('mediaselect', $(this).parents('li').data('movideoMediaItem'));
                });
            });
        }
        
        /*
            Navigation handler
            Bound to self.$nav elems
            @param e (Object)   Event object
        */
        function _handleNav(e) {
            e.preventDefault();
            
            // return if disabled
            if ($(this).data('disabled')) return;

            // get direction
            var direction = $(this).attr('rel');
            
            // trigger event
            _trigger(direction);
            
            // step list!
            _stepList(direction);
        }
        
        /*
            Steps list in self.stepIncrement value in specified direction
        */
        function _stepList(direction, travel) {
            // console.log('_stepList, currentPos:'+self.currentPos, 'step:'+self.stepIncrement, 'stepSize:'+self.o.stepSize, 'maxTravel:'+self.maxTravel);
            var travel, props;

            // stop current animation
            $(self.$lists).stop(true);
            
            // determine next list travel value
            if (direction == 'next') {
                if (self.currentPos > self.maxTravel)
                    travel = self.currentPos - self.stepIncrement;
                else if (self.currentPos == self.maxTravel)
                    travel = 0;
                // console.log('travel type',_getType(travel));
                if (travel < self.maxTravel || travel === undefined)
                    travel = self.maxTravel;
            }
            else {
                if (self.currentPos < 0)
                    travel = self.currentPos + self.stepIncrement;
                else if (self.currentPos == 0)
                    travel = self.maxTravel;
                if (travel > 0)
                    travel = 0;
            }
            
            // store new currentPos
            self.currentPos = travel;
            
            // console.log('_stepList, travel', travel);
            
            // define css properties obj to step to... YO!
            props = (self.o.display == 'wide') ? { left:travel } : { top:travel };
            
            // go time!
            $(self.$lists).each(function(index, value){ 
                value.animate(props, self.o.duration, self.o.easing); 
            });
            
            _updateNav();
        }
        
        /*
            Updates availability of navigation elems based on list position and count
        */
        function _updateNav() {
		    if (o.showNav === false)
                return;
            
            var disabledClass = self.o.nameSpace+'-nav-disabled';
            
            // if less items available than pageSize, disable...
             console.log('_updateNav, nav check', self.currentPos, self.count, self.o.pageSize, self.count <= (self.o.pageSize * self.o.rows),(self.o.pageSize * self.o.rows));
            if (self.count <= (self.o.pageSize * self.o.rows))
                self.$nav.addClass(disabledClass).data('disabled',true);
            else
                self.$nav.removeClass(disabledClass).data('disabled',false);
            
            // if looping is enabled.
            if (self.o.loop) return;

            // when at far left/top...
            if (self.currentPos == 0) {
                self.$navPrev
                    .addClass(disabledClass)
                    .data('disabled',true);
            }
            // when at far right/bottom...
            else if (self.currentPos <= self.maxTravel) {
                self.$navNext
                    .addClass(disabledClass)
                    .data('disabled',true);
            }
        }
        
        /*
            Show Drawer
            @param obj (Object) media item's object
        */
        function _showDrawer(obj) {
            // console.log('drawer',obj);
            
            // inject media data into drawer
            self.$drawerTitle.text( o.drawerTitleLimit && obj.title.length > o.drawerTitleLimit ? obj.title.substr(0,o.drawerTitleLimit)+'...' : obj.title );
            self.$drawerDesc.text( o.drawerDescLimit && obj.description.length > o.drawerDescLimit ? obj.description.substr(0,o.drawerDescLimit)+'...' : obj.description );
            
            // define animation properties obj based on o.drawerSide option...
            var travel = (o.drawerSide=='top' || o.drawerSide=='bottom') ? -self.$drawer.outerHeight() : -self.$drawer.outerWidth();
            var ani = {};
            ani[o.drawerSide] = travel;
            
            // animate
            self.$drawer
                .show()
                .stop(true)
                .animate(ani, o.drawerDuration, o.drawerEasing);
        }
        
        /*
            Hide Drawer
        */
        function _hideDrawer() {
            // define animation properties obj based on o.drawerSide option...
            var ani = {};
            ani[o.drawerSide] = 0;
            
            // animate
            self.$drawer
                .stop(true)
                .animate(ani, o.drawerDuration, o.drawerEasing);
        }
        
        /*
            *** DEPRECATED TO FACILITATE ALLOW THE CUSTOM NAMING OF widthViewport AND widthContainer OPTIONS ***
            Applies the passed style property from plugin options to passed $elem
            Only applies if the option value has a true value, otherwise it will fallback to CSS value
        */
        function _applyStyle($el, style) {
            if (o[style])
                $el.css(style, o[style]);
            return $el;
        }
        
        /*
            Show loading mask, trigger loading event
        */
        function _showLoading() {
            if (!self.$mask) return;
            _trigger('loading', self);
            self.$mask.fadeIn(100);
        }
        
        /*
            Hide loading mask, trigger loaded event
        */
        function _hideLoading() {
            if (!self.$mask) return;
            _trigger('loaded', self);
            self.$mask.fadeOut(100);
        }
        
        /*
            Merges options with defaults
            Sanitises options by running each option key's sanitiser method, $.fn.videobar.sanitiser[key](), if it exists...
            Replaces instance methods (this.buildMediaItem()) with custom methods passed in
            @param  opts (Object)
        */
        function _buildOptions(opts) {
            var sanitisers = $vb.sanitisers,
                opts = $.extend({}, $vb.defaults, opts || {});

            for (key in opts) {
                // run corresponding sanitiser method if avail
                if (sanitisers[key]) 
                    opts[key] = sanitisers[key](opts[key],opts);
                
                // replace instance methods with custom methods
                if ($.isFunction(opts[key]))
                    self[key] = opts[key];
            }
            return opts;
        }

        /*
            Triggers the specified event on the constructed $elem
            @param type (String)    Name of event to trigger
            @param data             Data to pass with event. Gets converted to an array and is
                                    passed as second param in event callback e.g. function(event,data){ ... }
        */
        function _trigger(type, data) {
            // console.log('trigger',o.nameSpace+type, data);
            data = data || [];
            data = (_getType(data) == 'Array') ? data : [data] || [];
			  $el.trigger(o.nameSpace+type, data);
        }
        
        /*
            Handle Error information by dispatching new error event
            @param type (String)    The type of error
            @param code             The error code, if available
            @param message          The error message, if available
            @param source           The source of the error
        */
        function _handleError(type, code, message, source) {
			var error = {
			    type: type,
				code: code,
				message: message,
				source: source
			};
			_trigger("error", error);
		}
        
        function _handleHttpError(response) {
            _handleError("http", response.code || 'Unknown error code', response.message || '', response);
        }

        // =================================
        // = Privileged / Instance Methods =
        // =================================
        
        this.buildMediaItem = function(obj) {
            var o = this.o,
                ns = o.nameSpace,
                $item = $('<li class="'+ ns +'-item" id="videobar-id-'+ obj.id +'">'),
                
                // parse item properties
                title = this.buildTitle(obj.title),
                desc = this.buildDesc(obj.description),
			    dur = MOVIDEO.utils.formatTime(obj.duration),
				link = this.buildLink(obj) || '#',
                img;
		   // parse image path
            if (obj.imagePath != '')
                img = obj.imagePath + (o.imageCrop?'cropped/':'') + o.imageWidth + 'x' + o.imageHeight+'.png';
            else
                img = o.imagePlaceHolder;
            var itemHTML;
			if(o.imagePlaceHolder!=null)
			   itemHTML = '<div class="'+ ns +'-item-inner" class="clearfix"><a class="'+ ns +'-item-link '+ ns +'-item-image-link" href="'+ link +'"><img src="'+ o.imagePlaceHolder +'" alt="'+ title +'" class="'+ ns +'-item-img" width="'+ o.imageWidth +'" height="'+ o.imageHeight +'" style="background-image:url('+ img +')" border="0"></a>';
			else
			   itemHTML = '<div class="'+ ns +'-item-inner" class="clearfix"><a class="'+ ns +'-item-link '+ ns +'-item-image-link" href="'+ link +'"><img src="'+ img +'" alt="'+ title +'" class="'+ ns +'-item-img" border="0"></a>';
			
         /*   var itemHTML = '<div class="'+ ns +'-item-inner" class="clearfix"><a class="'+ ns +'-item-link '+ ns +'-item-image-link" href="'+ link +'"><img src="'+ img +'" alt="'+ title +'" class="'+ ns +'-item-img" width="'+ o.imageWidth +'" height="'+ o.imageHeight +'" style="background-image:url('+ o.imagePlaceHolder +')" border="0"></a>';
           */     
            if (o.showTitle || o.showDesc || o.showDur)
                itemHTML += '<div class="'+ ns +'-item-info">';
            
            if (o.showTitle)
                itemHTML += '<p class="'+ ns +'-item-title"><a class="'+ ns +'-item-link" href="'+ link +'">'+ title +'</a></p>';
        
            if (o.showDesc)
                itemHTML += '<p class="'+ ns +'-item-desc">'+ desc +'</p>';
        
			if (o.showDur)
                  itemHTML += '<p class="'+ ns +'-item-dur">'+ dur +'</p>';
        
		
            if (o.showTitle || o.showDesc || o.showDur)
                itemHTML += '</div>';
            
            itemHTML += '</div>';
            
            // inject item html
            $item[0].innerHTML = itemHTML;
            
            // store item obj in item data
            $item.data('movideoMediaItem',obj);
            
            // bind click event to link elems
            $item.find('a').click(obj, this.mediaHandler);
            
            // only apply height to item if in wide display mode...
            // $item = (o.display == 'wide') ? _applyStyle($item, 'heightViewport') : $item;
            $item = (o.display == 'wide' && o.heightList) ? $item.css({ height:o.heightList }) : $item;
            
            return $item;
        };
        
        this.buildTitle = function(title) {
				if(!title)
					return "";
				
				if(o.titleLimit && title.length > o.titleLimit){							
					title = title.substring(0, o.titleLimit );
					title = title.replace(/\w+$/, '');
					title = title + "...";
				}
					
            return title;
        };
               
		 this.buildDesc = function(desc) {
				if(!desc)
					return "";
            return (o.descLimit && desc.length > o.descLimit) ? desc.substr(0,o.descLimit)+'...' : desc;
        };

        this.buildLink = function(obj) {
            return '#';
        };
        
        this.mediaHandler = function(event) {
            // set in options
            event.preventDefault();
        };
        
        this.next = function() {
            self.$navNext.click();
        };
        
        this.prev = function() {
            self.$navPrev.click();
        };
        
        this.reset = function() {
            // clear random timeout
            clearTimeout(self.timeout);

            // reset instance vars
            var vars = ['$listItems', 'count', 'currentPos', 'stepPx', 'stepIncrement', 'pagePx', 'maxTravel', 'listPx', 'timeout'],
                i = vars.length;
            while (i--)
                self[vars[i]] = null;
            
            // empty $list
            self.$list.empty();
            
            // reset $list pos
            var css = (self.o.display == 'wide') ? { left:0 } : { top:0 };
            self.$list.css(css);
        };
        
        this.updateMedia = function(id) {
            this.reset();
            _showLoading();
            id = $vb.sanitisers.mediaId(id);
            movideoGet.media(id);
        };
        
        this.updatePlaylist = function(id) {
            this.reset();
            _showLoading();
            movideoGet.playlist(id);
        };
        
		this.updateApiCall = function(call) {
            this.reset();
            _showLoading();
            movideoGet.apiCall(call);
        };
        
        // ====================
        // = Constructor Code =
        // ====================
        
        // merge options with defaults
        var o = this.o = _buildOptions(opts);
        // console.log('*** NICE OPTIONS',o);
        
        // Let's start by building the UI...
        _buildInterface();
        _attachEvents();

        // Now let's request a session and fetch some data!
        _requestSession(o.apiKey, o.appAlias, o.api);
        
    };
    // End Constructor


    // ==================
    // = Util Functions =
    // ==================

    /*
        Bullet-proof way of getting Object type
        http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
        @return (String)    A string representing the object type, e.g. 'Array', 'Object, ''String', etc.
    */
    function _getType(object) {
        return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
    }


    // ================================
    // = jQuery Plugin Initialisation =
    // ================================

    /*
        @param options (Object or String)
            Object
                A standard initialisation call with options passed in
                e.g. $(elem).videobar({ option:'value' });
            String
                A method call, where the method name is passed in as a string,
                and any arguments are passed in the second param
                e.g. $(elem).videobar('methodName');

    */
    $.fn.videobar = function(options) {
        
        // return if no elements found
        if (!this.length) return this;
        
        // TODO: return it already constructed

        var args = (arguments[1]) ? Array.prototype.slice.call(arguments,1) : null,
            inst,
            optType = _getType(options);

        this.each(function(){
            var $elem = $(this);
            if (optType == 'String') {
                inst = $elem.data('videobar');
                if (inst[options]) {
                    return inst[options].apply(inst,args);
                }
                else {
                    $.error('Method '+options+' does not exist on jQuery.videobar');
                }
            }
            else if (optType == 'Object') {
                // create and store instance in data of $elem
                if ($elem.data('videobar')) return this;
                inst = new VideoBar($elem, options);
                $elem.data('videobar', inst);
            }
            else {
                $.error('Please provide a valid mediaId, playlistId or api call to jQuery.videobar');
            }
        });

        return this;
        
    };


    /*
        Sanitiser methods for certain options, which ensure option values are as we expect, changing them if necessary...
        These methods are run in _buildOptions(), after options are merged with defaults        
    */
    $.fn.videobar.sanitisers = {

        mediaId: function(value,opts) {
            return (_getType(value) == 'Array') ? value.join(',') : value;
        },
        
        mediaLimit: function(value,opts) {
            value = (value < 1) ? 1 : value;
      		value = (value > 50) ? 50 : value;
      		return value;
        },
        
        mode: function(value,opts) {
            return (value == 'sequential' || value == 'random') ? value : 'sequential';
        },
        
        display: function(value,opts) {
            return (value == 'wide' || value == 'tall') ? value : 'wide';
        },
        
        stepSize: function(value,opts) {
            var pageSize = opts.pageSize;
            if (!value) {
                // if no value provided, stepSize == pageSize by default
                return pageSize;
            }
            else {
                // ensure value is no larger than pageSize
                return (!(value > pageSize) && !(value < 1)) ? value : pageSize;
            }
        },
        
        easing: function(value,opts) {
            return ($.easing[value]) ? value : 'swing';
        },
        
        drawerEasing: function(value,opts) {
            return ($.easing[value]) ? value : 'swing';
        }
        
    };

    /*
        Expose default options on the plugin namespace for easy app-wide overriding
            e.g.
            $.fn.videobar.defaults.api = '/path/to/different/api';
            $.fn.videobar.defaults.buildMediaItem = function(title){ ... };
    */
    $.fn.videobar.defaults = {
        
        // Movideo options
        appAlias:           null,
        apiKey:             null,
        api:                '/api/rest/',
        mediaId:            null,
        playlistId:         null,
        apiCall:            null,
        mediaLimit:         50,
        
        // Display options
        widthViewport:      null,           // (Integer) viewport width in pixels, false uses css value
        heightViewport:     null,           // (Integer) viewport height in pixels, false uses css value
        widthContainer:     null,
        heightContainer:    null,
        widthLoading:       null,           // (Integer) loading mask width in pixels, false uses the width of the element
        heightLoading:      null,           // (Integer) loading mask height in pixels, false uses the height of the element
        widthList:          null,           // (Integer) list width in pixels, false uses css value
        heightList:         null,           // (Integer) list height in pixels, false uses css value
        mode:               'sequential',   // or 'random'
        display:            'wide',         // or 'tall'
        rows:               1,              // when display: 'wide' the number of rows, when 'tall' the number of columns.
        pageSize:           5,              // number of items per 'page' i.e. items fully visible at once
        stepSize:           null,           // this value gets set to the pageSize value if none is passed in
        duration:           600,
        easing:             'easeInOutQuart',
        showNav:            true,
        loop:               true,
        randomDelay:        8000,
        
        // Media item display options
        imageCrop:          false,
        imageWidth:         100,
        imageHeight:        56,
        imagePlaceHolder:   null,
        showTitle:          true,
        showDesc:           true,
		showDur:           false,
		
        titleLimit:         20,             // false sets no char limit
        descLimit:          65,             // false sets no char limit
        
        // Drawer options
        showDrawer:         false,
        drawerSide:         'top',
        drawerTitleLimit:   false,
        drawerDescLimit:    false,
        drawerDuration:     200,
        drawerEasing:       'easeOutQuart',
        
        // Custom builders
        /*
            Override at instance level by passing build funcs during plugin initialisation
            Override at app-wide/constructor level by setting: $.fn.videobar.defaults.buildMediaItem = function(args){ ... }
        */
        buildMediaItem: null,
        buildLink:      null,
        buildTitle:     null,
        buildDesc:      null,
        mediaHandler:   null,
		noresultsHandler: null,
        
        // Class names
        nameSpace:      'videobar'
        
    };

})(jQuery);
