/*!
 * dragtable - jquery ui widget to re-order table columns 
 * version 3.0
 * 
 * Copyright (c) 2010, Jesse Baird <jebaird@gmail.com>
 * 12/2/2010
 * https://github.com/jebaird/dragtable
 * 
 * Licensed under the MIT (LICENSE.txt)
 * 
 * 
 * 
 * Forked from https://github.com/akottr/dragtable - Andres Koetter akottr@gmail.com
 * 
 *
 * 
 * 
 * quick down and and dirty on how this works
 * ###########################################
 * so when a column is selected we grab all of the cells in that row and clone append them to a semi copy of the parent table and the 
 * "real" cells get a place holder class witch is removed when the dragstop event is triggered
 * 
 * 
 * make it easy to have a button swap columns
 * 
 * 
 * Events - in order of trigger
 * dragtablestart - when the user mouses down on handle or th, use in favor of display helper
 * dragtablebeforeChange - called when a col will be moved
 * dragtablechange - called after the col has been moved
 * dragtablestop - the user mouses up and stops dragging and the drag display is removed from the dom
 * 
 * 
 * 
 * 
 * IE notes
 *  ie8 in quirks mode will only drag once after that the events are lost
 * 
 */

 (function($) {
  $.widget("jb.dragtable", {

        eventWidgetPrefix: 'dragtable',

        options: {
            //used to the col headers, data contained in here is used to set / get the name of the col
            dataHeader:'data-header',
            //class name that handles have 
            handle:'dragtable-drag-handle',
            //draggable items in cols, .dragtable-drag-handle has to match the handle options
            items: 'th:not( :has( .dragtable-drag-handle ) ), .dragtable-drag-handle',
            //if a col header as this class, cols cant be dragged past it
            boundary: 'dragtable-drag-boundary',
            //classnames that get applied to the real td, th
            placeholder: 'dragtable-col-placeholder',
            
            /*
                the drag display will be appended to this element, 
                if this is set to  document.body and has been zeroed off the display will seem to jump

                Its either : parent or a dom node
            */
            appendTarget: ':parent',
            //if true,this will scroll the appendTarget offsetParent when the dragDisplay is dragged past its boundaries
            scroll: false
            
        },
        // when a col is dragged use this to find the semantic elements, for speed
        tableElemIndex:{  
            head: '0',
            body: '1',
            foot: '2'
        },
        tbodyRegex: /(tbody|TBODY)/,
        theadRegex: /(thead|THEAD)/,
        tfootRegex: /(tfoot|TFOOT)/,
        
        _create: function() {
            
            //console.log(this);
            //used start/end of drag
            this.startIndex = null;
            this.endIndex = null;
            //the references to the table cells that are getting dragged
            this.currentColumnCollection = [];
            //the references the position of the first element in the currentColunmCollection position
            this.currentColumnCollectionOffset = {};
            //the div wrapping the drag display table
            this.dragDisplay = $([])

            
            var self = this,
                o = self.options,
                el = self.element;
            
            o.appendTarget = ( o.appendTarget === ':parent' ) ? this.element.parent() : $( o.appendTarget );
            

            //grab the ths and the handles and bind them 
            el.delegate(o.items, 'mousedown.' + self.widgetEventPrefix, function(e){
                
                var $handle = $(this),
                    elementOffsetTop = self.element.position().top;

                //make sure we are working with a th instead of a handle
                if( $handle.hasClass( o.handle ) ){
                    
                    $handle = $handle.closest('th');
                    //change the target to the th, so the handler can pick up the offsetleft
                    e.currentTarget = $handle.closest('th')[0]
                }
                
                
                self.getCol( $handle.index() )
                    .attr( 'tabindex', -1 )
                    .focus()
                    .disableSelection()
                    .css({
                        top: elementOffsetTop,
                            //need to account for the scroll left of the append target, other wise the display will be off by that many pix
                            left: ( self.currentColumnCollectionOffset.left + o.appendTarget[0].scrollLeft )
                        })
                    .appendTo( o.appendTarget )
                

                
                self._mousemoveHandler( e );
                //############
            });

        },

        /*
         * e.currentTarget is used for figuring out offsetLeft
         * getCol must be called before this is 
         * 
         */
         _mousemoveHandler: function( e ){
            //call this first, catch any drag display issues
            this._start( e )
            
            var self = this,
                o = self.options,
                prevMouseX = e.pageX,
                dragDisplayWidth = self.dragDisplay.outerWidth(),
                halfDragDisplayWidth = dragDisplayWidth / 2,
                appendTargetOP = o.appendTarget.offsetParent()[0],
                scroll = o.scroll,
                //get the col count, used to contain col swap
                colCount = self.element[ 0 ]
                    .getElementsByTagName( 'thead' )[ 0 ]
                    .getElementsByTagName( 'tr' )[ 0 ]
                    .getElementsByTagName( 'th' )
                    .length - 1;

                $( document ).bind('mousemove.' + self.widgetEventPrefix, function( e ){
                    var columnPos = self._setCurrentColumnCollectionOffset(),
                        mouseXDiff = e.pageX - prevMouseX,
                        appendTarget = o.appendTarget[0],
                        left =  ( parseInt( self.dragDisplay[0].style.left ) + mouseXDiff  );
                        self.dragDisplay.css( 'left', left )
                    
               /*
                * when moving left and e.pageX and prevMouseX are the same it will trigger right when moving left
                * 
                * it should only swap cols when the col dragging is half over the prev/next col
                */                    
                if( e.pageX  < prevMouseX ){
                   //move left
                   var threshold = columnPos.left - halfDragDisplayWidth;
                   
                   
                   //scroll left
                   if( left < ( appendTarget.clientWidth  - dragDisplayWidth ) && scroll == true ) {
                    var scrollLeft =  appendTarget.scrollLeft + mouseXDiff
                        /*
                         * firefox does scroll the body with target being body but chome does
                         */
                         if( appendTarget.tagName == 'BODY' ) {
                            window.scroll( window.scrollX + scrollLeft, window.scrollY );
                        } else {
                            appendTarget.scrollLeft = scrollLeft;
                        }
                        
                    }
                    
                    
                    if( left  < threshold ){
                        self._swapCol(self.startIndex-1);
                    }

                }else{
                 //move right
                 var threshold = columnPos.left + halfDragDisplayWidth ;
                 
                    //scroll right
                    if( left > (appendTarget.clientWidth - dragDisplayWidth ) && scroll == true ) {
                        //console.log(  o.appendTarget[0].clientWidth + (e.pageX - prevMouseX))
                        
                        var scrollLeft =  appendTarget.scrollLeft + mouseXDiff
                        /*
                         * firefox does scroll the body with target being body but chome does
                         */
                         if( appendTarget.tagName == 'BODY' ) {
                            window.scroll( window.scrollX + scrollLeft, window.scrollY );
                        } else {
                            appendTarget.scrollLeft = scrollLeft;
                        }
                        
                    }
                    
                    //move to the right only if x is greater than threshold and the current col isn' the last one
                    if( left  > threshold  && colCount != self.startIndex ){
                        self._swapCol( self.startIndex + 1 );
                    }
                }
                //update mouse position
                prevMouseX = e.pageX;
                
            })
            .one( 'mouseup.' + self.widgetEventPrefix ,function(e ){
                self._stop( e );
            });

            },

            _start: function( e ){
                
                $( document )
                    //move disableselection and cursor to default handlers of the start event
                    .disableSelection()
                    .css( 'cursor', 'move');

                    // guess the width of the column that is getting dragged and apply it to the drag display. fixes issues with
                    // cols / tables having fixed widths
                    this.dragDisplay.width( this.currentColumnCollection[0].clientWidth )

                    return this._eventHelper('start',e);
                    
                },
                _stop: function( e ){
                    
                    // issue #25 reorder the stop event order always remove the stop event 
                    $( document )
                        .unbind( 'mousemove.' + this.widgetEventPrefix )
                        .enableSelection()
                        .css( 'cursor', '');

                    // clean up
                    this
                        .dropCol()
                        .dragDisplay.remove();
                    // let the world know we have stopped
                    this._eventHelper('stop',e,{});

                   
               },
               
               _setOption: function(option, value) {
                $.Widget.prototype._setOption.apply( this, arguments );
                
            },
            
        /*
         * get the selected index cell out of table row
         * needs to work as fast as possible. and performance gains in this method are worth the time
         *  because its used to build the drag display and get the cells on col swap
         * http://jsperf.com/binary-regex-vs-string-equality/4
         */
         _getCells: function( elem, index ){
            //console.time('getcells');
            var td,
                parentNodeName,
                ei = this.tableElemIndex,
                //TODO: clean up this format 
                tds = {
                    //store where the cells came from
                    'semantic':{
                        '0': [],//head throws error if ei.head or ei['head']
                        '1': [],//body
                        '2': []//footer
                    },
                    //keep a ref in a flat array for easy access
                    'array':[]
                },
                //cache regex, reduces looking up the chain
                theadRegex = this.theadRegex,
                tbodyRegex = this.tbodyRegex,
                tfootRegex = this.tfootRegex,

                
                tdsSemanticHead = tds.semantic[ei.head],
                tdsSemanticBody = tds.semantic[ei.body],
                tdsSemanticFoot = tds.semantic[ei.foot];
                
            //console.log(index);
            //check does this col exsist
            if(index <= -1 || typeof elem.rows[0].cells[index] == undefined){
                return tds;
            }
            
            for(var i = 0, length = elem.rows.length; i < length; i++){
                
                td = elem.rows[i].cells[index];
                
                //if the row has no cells dont error out;
                if( td == undefined ){
                    continue;
                }
                
                parentNodeName = td.parentNode.parentNode.nodeName;
                tds.array.push(td);
                //faster to leave out ^ and $ in the regular expression
                if( tbodyRegex.test( parentNodeName ) ){
                    
                    tdsSemanticBody.push( td );
                    
                }else if( theadRegex.test( parentNodeName ) ){
                    
                    tdsSemanticHead.push( td );
                    
                }else if( this.tfootRegex.test( parentNodeName ) ){
                    
                    tdsSemanticFoot.push( td );
                }
                
                
            }

            return tds;
        },
        /*
         * returns all element attrs in a string key="value" key2="value"
         */
         _getElementAttributes: function(element){
            
            var attrsString = [],
                attrs = element.attributes,
                i = 0,
                length = attrs.length;

            for( ; i < length; i++) {
                attrsString.push( attrs[i].nodeName + '="' + attrs[i].value+'"' );
            }
            return attrsString.join(' ');
        },

        /*
         * faster than swap nodes
         * only works if a b parent are the same, works great for columns
         */
         _swapCells: function(a, b) {
            a.parentNode.insertBefore(b, a);
        },

        /*
         * used to trigger optional events
         */
         _eventHelper: function(eventName ,eventObj, additionalData){

            return this._trigger( 
                eventName, 
                eventObj, 
                $.extend({
                    column: this.currentColumnCollection,
                    order: this.order(),
                    startIndex: this.startIndex,
                    endIndex: this.endIndex,
                    dragDisplay: this.dragDisplay,
                    columnOffset: this.currentColumnCollectionOffset            
                },additionalData)
                );
        },





        /*
         * build copy of table and attach the selected col to it, also removes the select col out of the table
         * @returns copy of table with the selected col
         * 
         * populates self.dragDisplay
         * TODO: name this something better, like select col or get dragDisplay
         * 
         */     
        getCol: function(index){
            //console.log('index of col '+index);
            //drag display is just simple html
            
            var target, 
                cells,
                clone,
                tr,
                i,
                length,
                $table = this.element,
                self = this,
                eIndex = self.tableElemIndex,
                placholderClassnames = ' ' + this.options.placeholder;;
            
                //BUG: IE thinks that this table is disabled, dont know how that happend
            self.dragDisplay = $('<table '+self._getElementAttributes($table[0])+'></table>')
                .addClass('dragtable-drag-col');
                
            //start and end are the same to start out with
            self.startIndex = self.endIndex = index;
            

            cells = self._getCells($table[0], index);
            self.currentColumnCollection = cells.array;
            
            //################################
            
            //TODO: convert to for in // its faster than each
            $.each(cells.semantic,function(k,collection){
                //dont bother processing if there is nothing here
                
                if(collection.length == 0){
                    return;
                }
                
                if ( k == '0' ){
                    target = document.createElement('thead');
                    self.dragDisplay[0].appendChild(target);

                }else if ( k == 1 ) { 
                    target = document.createElement('tbody');
                    self.dragDisplay[0].appendChild(target);

                }else {
                    target = document.createElement('tfoot');
                    self.dragDisplay[0].appendChild(target);
                }

                for(i = 0,length = collection.length; i < length; i++){
                    
                    clone = collection[i].cloneNode(true);
                    collection[i].className+=placholderClassnames;
                    tr = document.createElement('tr');
                    tr.appendChild(clone);

                    target.appendChild(tr);
                    
                }
            });


            this._setCurrentColumnCollectionOffset();


            self.dragDisplay  = $('<div class="dragtable-drag-wrapper"></div>').append(self.dragDisplay)
            return self.dragDisplay;
        },


        _setCurrentColumnCollectionOffset: function(){
            return this.currentColumnCollectionOffset = $( this.currentColumnCollection[0] ).position();
        },

        /*
         * move column left or right
         */
         _swapCol: function( to ){
            
            //cant swap if same position
            if(to == this.startIndex){
                return false;
            }
            
            var from = this.startIndex;
            this.endIndex = to;
            //this col cant be moved past me
            var th = this.element.find('th').eq( to );
            //check on th
            if( th.hasClass( this.options.boundary ) == true ){
                return false;
            }
            //check handle element
            if( th.find( '.' + this.options.handle ).hasClass( this.options.boundary ) == true ){
                return false;
            }
            
            if( this._eventHelper('beforeChange',{}) === false ){
                return false;
            };
            
            
            if(from < to) {
                //console.log('move right');
                for(var i = from; i < to; i++) {
                    var row2 = this._getCells(this.element[0],i+1);
                //  console.log(row2)
                for(var j = 0, length = row2.array.length; j < length; j++){
                    this._swapCells(this.currentColumnCollection[j],row2.array[j]);
                }
            }
        } else {
                //console.log('move left');
                for(var i = from; i > to; i--) {
                    var row2 = this._getCells(this.element[0],i-1);
                    for(var j = 0, length = row2.array.length; j < length; j++){
                        this._swapCells(row2.array[j],this.currentColumnCollection[j]);
                    }
                }
            }
            this._eventHelper('change',{});
            
            this.startIndex = this.endIndex;
        },
        /*
         * called when drag start is finished
         */
         dropCol: function(){
            //TODO: cache this when the option is set
            var regex = new RegExp("(?:^|\\s)" + this.options.placeholder + "(?!\\S)",'g');
            //remove placeholder class
            //dont use jquery.fn.removeClass for performance reasons
            for(var i = 0, length = this.currentColumnCollection.length; i < length; i++){
                var td = this.currentColumnCollection[i];
                
                td.className = td.className.replace(regex,'')
            }
            
            return this;

        },
        /*
         * get / set the current order of the cols
         */
         order: function(order){
            var self = this,
                elem = self.element,
                options = self.options,
                headers = elem.find('thead tr:first').children('th');
            
            
            if(order == undefined){
                //get
                var ret = [];
                headers.each(function(){
                    var header = this.getAttribute(options.dataHeader);
                    if(header == null){
                        //the attr is missing so grab the text and use that
                        header = $(this).text();
                    }
                    
                    ret.push(header);
                    
                });
                
                return ret;
                
            }else{
                //set
                //headers and order have to match up
                if(order.length != headers.length){
                    
                    return self;
                }
                for(var i = 0, length = order.length; i < length; i++){
                   
                   var start = headers.filter('['+ options.dataHeader +'='+ order[i] +']').index();
                   if(start != -1){
                        
                        self.startIndex = start;
                        
                        self.currentColumnCollection = self._getCells(self.element[0], start).array;

                        self._swapCol(i);
                    }
                    
                    
                }
                return self;
            }
        },
        
        destroy: function() {
            var self = this,
                o = self.options;
            
            this.element.undelegate( o.items, 'mousedown.' + self.widgetEventPrefix );
            
            $( document ).unbind('.' + self.widgetEventPrefix )
            
        }

        
    });

})(jQuery);
