/**
 * jQuery makeCollapsible
 *
 * This will enable collapsible-functionality on all passed elements.
 * Will prevent binding twice to the same element.
 * Initial state is expanded by default, this can be overriden by adding class
 * "mw-collapsed" to the "mw-collapsible" element.
 * Elements made collapsible have class "mw-made-collapsible".
 * Except for tables and lists, the inner content is wrapped in "mw-collapsible-content".
 *
 * @author Krinkle <krinklemail@gmail.com>
 * @author Lupo    https://commons.wikimedia.org/wiki/User:Lupo
 *
 * Dual license:
 * @license CC-BY 3.0 <http://creativecommons.org/licenses/by/3.0>
 * @license GPL2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
 */

( function( $, mw ) {

// Warning: this code employs a number of speed optimizations to avoid excessive runtime on older
// browsers to set up the initial state if there are many collapsible, initially collapsed elements
// on the page.
//   A particular problem is that this module uses classes to record the collapsed/expanded state of
// items. Modifying classes may cause reflows of the page rendering, which may make subsequent hide
// operations slow because they need to recalculate the computed styles of elements. Also, simply
// triggering the installed events to actually collapse the elements may cause significant overhead,
// even if event bubbling is bypassed through triggerHandler() instead of trigger(). Finally, when
// setting up the initial collapsed elements, no animations must be used.
//   The code below takes care of all this. Initial setup runs in two phases: in a first phase, we only
// collect all the modifications in a "data" object without making any changes to the DOM; then in a
// second phase, we play back all the recorded modifications. During phase one, we do not trigger events
// on togglers but invoke the installed toggle* function directly.
//   Once the setup is done, normal event handling will typically invoke events without "data" objects,
// and will then perform DOM modifications right away, which should be OK since any click on a toggler
// will change only one collapsible element, in which case the performance should be still good enough.
// Performance becomes only a problem if we want to toggle several collapsible elements at once.
//   Because we know that we only have a "data" object during initial setup, where we at most want to collapse
// some elements, the handling of collapse/expand is slightly asymmetric in the toggle* handlers as we apply
// class changes directly when expanding.

var rspace = /\s+/;

// Remove and add some classes in one step.
$.fn.changeClass = function( fromClass, toClass ) {
	if ( !fromClass ) {
		return this.addClass( toClass );
	}
	if ( !toClass ) {
		return this.removeClass( fromClass );
	}
	var classNames, fromNames = fromClass.split( rspace ), toNames = toClass.split( rspace ), elem, classes, i, j, l;
	for ( i = 0, l = this.length; i < l; i++ ) {
		elem = this[i];
		classNames = ( elem.className || "" ).split ( ' ' ) || [];
		classes = {};
		for ( j = 0; j < classNames.length; j++ ) {
			if ( classNames[j] ) {
				classes[classNames[j]] = true;
			}
		}
		for ( j = 0; j < fromNames.length; j++ ) {
			if ( fromNames[j] && classes[fromNames[j]] ) {
				classes[fromNames[j]] = false;
			}
		}
		for ( j = 0; j < toNames.length; j++ ) {
			if ( toNames[j] ) {
				classes[toNames[j]] = true;
			}
		}
		classNames = "";
		for ( j in classes ) {
			if ( classes[j] ) {
				classNames += j + ' ';
			}
		}
		elem.className = jQuery.trim( classNames );
		classes = null;
	}
	return this;
};

function collapse( $collapsible, $defaultToggle, immediateTransition, data ) {
	var $containers;
	var op = 'slide';
	if ( !data ) {
		data = { immediate: [], fade: [], slide: [] }; // Use plain arrays.
	}
	if ( $collapsible.is( 'table' ) ) {
		// Hide all table rows of this table
		// Slide doesn't work with tables, but fade does as of jQuery 1.1.3
		// http://stackoverflow.com/questions/467336#920480
		$containers = $collapsible.children( 'tbody' ).children ( 'tr' );
		if ( $defaultToggle ) {
			// Exclude table row containing toggle link
			$containers = $containers.not( $defaultToggle.parent().parent() );
		}
		op = 'fade';
	} else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) {
		$containers = $collapsible.children( 'li' );
		if ( $defaultToggle ) {
			// Exclude list item containing toggle link
			$containers = $containers.not( $defaultToggle.parent() );
		}
	} else { // <div>, <p> etc.
		$containers = $collapsible.children( '.mw-collapsible-content' );

		if ( !$containers.length ) {
			// Otherwise assume this is a customcollapse with a remote toggle
			// .. and there is no collapsible-content because the entire element should be toggled
			if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
				op = 'fade';
			}
			$containers =	$collapsible;
		}
	}
	if ( immediateTransition ) {
		op = 'immediate';
	}
	data[op] = data[op].concat ( jQuery.makeArray( $containers ) );
	return data;
}

function expand( $collapsible, $defaultToggle, immediateTransition, data ) {
	var $containers;
	var op = 'slide';
	if (!data) {
		data = { immediate: [], fade: [], slide: [] };
	}
	if ( $collapsible.is( 'table' ) ) {
		$containers = $collapsible.children( 'tbody' ).children ( 'tr' );
		if ( $defaultToggle ) {
			// Exclude table row containing toggle
			$containers = $containers.not( $defaultToggle.parent().parent() );
		}
		op = 'fade';
	} else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) {
		$containers = $collapsible.children( 'li' );
		if ( $defaultToggle ) {
			// Exclude list-item containing togglelink
			$containers = $containers.not( $defaultToggle.parent() );
		}
	} else { // <div>, <p> etc.
		$containers =  $collapsible.children( '.mw-collapsible-content' );

		// If a collapsible-content is defined, collapse it
		if (! $containers.length ) {
			if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
				op = 'fade'
			}
			$containers = $collapsible;
		}
	}
	if ( immediateTransition ) {
		op = 'immediate';
	}
	data[op] = data[op].concat ( jQuery.makeArray( $containers ) );
	return data;
}

function hide( data ) {
	$( data.immediate ).hide();
	$( data.fade ).stop( true, true ).fadeOut();
	$( data.slide ).stop( true, true ).slideUp();
}

function show( data ) {
	$( data.immediate ).show();
	$( data.fade ).stop( true, true ).fadeIn();
	$( data.slide ).stop( true, true ).slideDown();
}


function toggleLinkPremade( $that, e, $collapsible, data ) {
	if (!data) {
		data = { immediate: false, doIt: true };
	}
	if ( data.doIt ) {
		$collapsible.toggleClass( 'mw-collapsed' );
	}
	if ( e ) {
		if ( $(e.target).is('a') ) {
			return true;
		}
		e.preventDefault();
		e.stopPropagation();
	}
	// It's expanded right now
	if ( !$that.hasClass( 'mw-collapsible-toggle-collapsed' ) ) {
		// Change toggle to collapsed
		if ( data.toggles ) {
			data.toggles = data.toggles.concat( jQuery.makeArray( $that ) );
		} else {
			$that.changeClass( 'mw-collapsible-toggle-expanded', 'mw-collapsible-toggle-collapsed' );
		}
		// Collapse element
		data.lists = collapse( $collapsible, $that, data.immediate, data.lists );
		if ( data.doIt ) {
			hide( data.lists );
		}

	// It's collapsed right now
	} else {
		// Change toggle to expanded
		$that.changeClass( 'mw-collapsible-toggle-collapsed', 'mw-collapsible-toggle-expanded' );
		// Expand element
		data.lists = expand( $collapsible, $that, data.immediate, data.lists );
		if ( data.doIt ) {
			show( data.lists );
		}
	}
}

function toggleLinkCustom( $that, e, $collapsible, data ) {
	if (!data) {
		data = { immediate: false, doIt: true };
	}
	if (e) {
		e.preventDefault();
		e.stopPropagation();
	}
	// Get current state and toggle to the opposite
	if ( data.doIt ) {
		$collapsible.toggleClass( 'mw-collapsed' );
	}
	if ( !data.doIt || $collapsible.hasClass( 'mw-collapsed' ) ) {
		data.lists = collapse( $collapsible, $that, data.immediate, data.lists );
		if ( data.doIt ) {
			hide( data.lists );
		}
	} else {
		data.lists = expand( $collapsible, $that, data.immediate, data.lists );
		if ( data.doIt ) {
			show( data.lists );
		}
	}
}

$.fn.makeCollapsible = function() {

	var defaultCollapseText = mw.msg( 'collapsible-collapse' );
	var defaultExpandText = mw.msg( 'collapsible-expand' );
	var data = { immediate: true, doIt: false, toggles: [], change: [] }; // Record changes; use plain arrays

	var $result = this
		.not( '.mw-made-collapsible' )                      // Exclude already handled elements
		.addClass ( 'mw-collapsible mw-made-collapsible' )  // case: $( '#myAJAXelement' ).makeCollapsible()
		.each(function() {
		var _fn = 'jquery.makeCollapsible> ';

		// Define reused variables and functions
		var	$that = $(this),
			that = this,
			collapsetext = $that.attr( 'data-collapsetext' ) || defaultCollapseText,
			expandtext = $that.attr( 'data-expandtext' ) || defaultExpandText;

		function toggleLinkDefault( $that, e, $collapsible, data ) {
			if (!data) {
				data = { immediate: false, doIt: true, toggles : []};
			}
			if (e) {
				e.preventDefault();
				e.stopPropagation();
			}
			if ( data.doIt ) {
				$collapsible.toggleClass( 'mw-collapsed' );
			}
			// It's expanded right now
			if ( !$that.hasClass( 'mw-collapsible-toggle-collapsed' ) ) {
				// Change link to "Show"
				if ( data.toggles ) {
					data.toggles = data.toggles.concat (jQuery.makeArray( $that ) );
					data.change[data.change.length] = { $item: $that, text: "" + expandtext };
				} else {
					$that.changeClass( 'mw-collapsible-toggle-expanded', 'mw-collapsible-toggle-collapsed' );
					var $lks = $that.children( 'a' );
					( $lks.length ? $lks : $that ).text( expandtext );
				}
				// Collapse element
				data.lists = collapse ($collapsible, $that, data.immediate, data.lists );
				if ( data.doIt ) {
					hide( data.lists );
				}

			// It's collapsed right now
			} else {
				// Change link to "Hide"
				$that.changeClass( 'mw-collapsible-toggle-collapsed', 'mw-collapsible-toggle-expanded' );
				var $lks = $that.children( 'a' );
				( $lks.length ? $lks : $that ).text( collapsetext );
				// Expand element
				data.lists = expand( $collapsible, $that, data.immediate, data.lists );
				if ( data.doIt ) {
					show( data.lists );
				}
			}
		}

		// Create toggle link with a space around the brackets (&nbsp;[text]&nbsp;)
		function createToggleLink ($collapsible) {
			return $( '<a href="#"></a>' )
				.text( collapsetext )
				.wrap( '<span class="mw-collapsible-toggle"></span>' )
				.parent()
				.prepend( '&nbsp;[' )
				.append( ']&nbsp;' )
				.bind( 'click.mw-collapse', function( e ) {
					toggleLinkDefault( $(this), e, $collapsible );
				} );
		}

		// Avoid event management overhead by recording in func the installed click handler, and invoke it directly.
		// That appears to be faster than even using $.triggerHandler() on the toggle link.
		var $toggleLink = null, func;

		// Check if this element has a custom position for the toggle link
		// (ie. outside the container or deeper inside the tree)
		// Then: Locate the custom toggle link(s) and bind them
		if ( ( $that.attr( 'id' ) || '' ).indexOf( 'mw-customcollapsible-' ) === 0 ) {

			var thatId = $that.attr( 'id' ),
				  $customTogglers = $( '.' + thatId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) );
			mw.log( _fn + 'Found custom collapsible: #' + thatId );

			// Double check that there is actually a customtoggle link
			if ( $customTogglers.length ) {
				$customTogglers.bind( 'click.mw-collapse', function( e ) {
					toggleLinkCustom( $(this), e, $that );
				} );
				$toggleLink = $customTogglers;
			} else {
				mw.log( _fn + '#' + thatId + ': Missing toggler!' );
			}
			func = toggleLinkCustom;

		// If this is not a custom case, do the default: wrap the contents add the toggle link
		// Elements are treated differently
		} else if ( $that.is( 'table' ) ) {
			// The toggle-link will be in one the the cells (td or th) of the first row
			var	$firstRowCells = $that.children ( 'tbody' ).children( 'tr:first' ).children ('th, td'),
				$toggle = $firstRowCells.children( '.mw-collapsible-toggle' );

			// If theres no toggle link, add it to the last cell
			if ( !$toggle.length ) {
				$firstRowCells.eq(-1).prepend( $toggleLink = createToggleLink( $that ) );
				func = toggleLinkDefault;
			} else {
				$toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function( e ) {
					toggleLinkPremade( $toggle, e, $that );
				} );
				func = toggleLinkPremade;
			}

		} else if ( $that.is( 'ul' ) || $that.is( 'ol' ) ) {
			// The toggle-link will be in the first list-item
			var	$firstItem = $that.children( 'li:first' ),
					$toggle = $firstItem.children( '.mw-collapsible-toggle' );

			// If theres no toggle link, add it
			if ( !$toggle.length ) {
				// Make sure the numeral order doesn't get messed up, force the first (soon to be second) item
				// to be "1". Except if the value-attribute is already used.
				// If no value was set WebKit returns "", Mozilla returns '-1', others return null or undefined.
				var firstval = $firstItem.attr( 'value' );
				if ( firstval === undefined || !firstval || firstval == '-1' ) {
					$firstItem.attr( 'value', '1' );
				}
				$toggleLink = createToggleLink( $that );
				$that.prepend( $toggleLink.wrap( '<li class="mw-collapsible-toggle-li"></li>' ).parent() );
				func = toggleLinkDefault;
			} else {
				$toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function( e ) {
					toggleLinkPremade( $toggle, e, $that );
				} );
				func = toggleLinkPremade;
			}

		} else { // <div>, <p> etc.

			// The toggle-link will be the first child of the element
			var $toggle = $that.children( '.mw-collapsible-toggle' );

			// If a direct child .content-wrapper does not exists, create it
			if ( !$that.children( '.mw-collapsible-content' ).length ) {
				$that.wrapInner( '<div class="mw-collapsible-content"></div>' );
			}

			// If there's no toggle link, add it
			if ( !$toggle.length ) {
				$that.prepend( $toggleLink = createToggleLink( $that ) );
				func = toggleLinkDefault;
			} else {
				$toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function( e ) {
					toggleLinkPremade( $toggle, e, $that );
				} );
				func = toggleLinkPremade;
			}
		}
		// If the initial state is collapsed, collapse it.
		if ( $that.hasClass( 'mw-collapsed' ) && $toggleLink ) {
			func( $toggleLink.eq(0), null, $that, data );
		}
	} );
	// Replay recorded changes. (The "data" closure variable is passed around by reference, so the toggle click handlers can
	// return back their changes through it.) data.lists now contains all DOM elements to be collapsed
	if ( data.lists ) {
		var oldOff = jQuery.fx.off; jQuery.fx.off = true; // Make sure we don't get any animations
		hide( data.lists );
		jQuery.fx.off = oldOff;
	}
	$( data.toggles ).changeClass( 'mw-collapsible-toggle-expanded', 'mw-collapsible-toggle-collapsed' );
	for ( var i = 0; i < data.change.length; i++ ) {
		var $lks = data.change[i].$item.children( 'a' );
		if ( !$lks.length ) {
			$lks = data.change[i].$item;
		}
		$lks.text( data.change[i].text );
	};
	return $result; // Subset of items passed in that were now enabled.
};
} )( jQuery, mediaWiki );
