/*
Copyright (c) 2009-10 Brian Dillard
Brian Dillard | Project Lead | b@briandillard.com | http://b@briandillard.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Requires jQuery 1.4+ and Pure JavaScript Templates 2.0+

*/

( function( $ ) {

	/**
	 * A method for binding custom events to a menu built out of a standard HTML elements.
	 * The menu HTML can be constructed in HTML or built on the fly using PURE JavaScript templates.
	 */

	$.fn.magicmenu = function( sections, options ) {

		//grab outer container

		var $node = $( this ).eq( 0 );

		//bail if collection empty or required arguments missing

		if ( !sections || sections.length === 0 ||
			$node.length === 0
		) {
			//log an error
			return this;
		}

		//merge user-defined localOpts with default ones

		var opts = $.extend( true, {}, $.fn.magicmenu.defaults, options );

		/* flags and pointers used during event handlers */

		var mouseTimer = null;
		var isVisible = true;

		/* inner helper functions */

		var getListSelector = function( section ) {
			return opts.selectors.list.replace(
				"${domId}", section.domId
			);
		};
		var getCurrentSelector = function( section ) {
			return opts.selectors.current
				.replace(
					"${domId}", section.domId
				)
				.replace(
					"${current}", section.current
				)
			;
		};

		var toggleVisibility = function( showIt ) {
			if ( !showIt ) {
				$( opts.selectors.all_lists ).toggleClass( opts.classes.active, false );
			}

			isVisible = showIt;

			$node.toggleClass( opts.classes.invisibility, !showIt );
		};

		var hideIt = function( now ) {
			var duration = now ? 0 : 750;

			mouseTimer = setTimeout( function() {
				toggleVisibility( false );
			}, duration );
		};

		var showIt = function() {
			if ( mouseTimer ) {
				clearTimeout( mouseTimer );
				mouseTimer = null;
			}
			toggleVisibility( true );
		};

		var readKeystroke = function( evt ) {
			var NOW = true;

			if ( evt && evt.keyCode === opts.keyCode ) {
				isVisible ? hideIt( NOW ) : showIt();
			}
		};

		/* Build any HTML that needs to be generated by the plugin. */
		
		if ( opts.buildHTML ) {
			adj.dom.extractAndRenderTemplate(
				"#template-magicmenu", sections, opts.directive
			);
		}

		/**
		 * Move our $node context to the actual magicmenu, not the node enclosing it.
		 * While we're at it, add any additional classes we need to for styling.
		 */
		$node = $node
			.find( opts.selectors.outer )
			.addClass( opts.classes.outer )
		;
		
		/**
		 * Now we can iterate through our sections and apply behaviors to each
		 * section of the magicmenu interface
		 */
		$.each( sections, function( i, section ) {

			var $list = $node.find(
				getListSelector( section )
			);

			var callbacks = $.extend( {}, opts.defaultCallbacks, section.callbacks );

			//run initialization routines

			callbacks.before_init.call(
				$node, section, $list
			);

			opts.events.init.call(
				$node, section, $list, opts, getCurrentSelector
			);

			callbacks.after_init.call(
				$node, section, $list
			);

			//bind the event handlers

			$list.find( opts.selectors.header ).click( function() {
				
				callbacks.header_before.call( this, section, $list, opts );
				opts.events.header_click.call( this, section, $list, opts );
				callbacks.header_after.call( this, section, $list, opts );
				
				return false;

			} );

			$list.find( opts.selectors.choice ).click( function() {

				callbacks.choice_before.call( this, section, $list, opts );
				opts.events.choice_click.call( this, section, $list, opts );
				callbacks.choice_after.call( this, section, $list, opts );
				
				return false;

			} );

		} );

		switch( opts.showMode ) {

			//hide the container by default and show it upon mouseover

			case "hover":

				//todo add effects and make them separate callbacks for configurability

				$node
					.addClass( opts.classes.invisibility )
					.mouseover( function() {
						showIt();
					} )
					.mouseout( function() {
						hideIt();
					} )
				;
				isVisible = false;

			break;

			//hide the container by default and show it upon a specific key event

			case "keystroke":

				$node.addClass( opts.classes.invisibility );

				isVisible = false;

				$( document ).keyup( readKeystroke );

			break;

			//menu is visible at all times

			default:

				//let the CSS handle it

			break;
		}

		//return object for chainability

		return this;

	};

	$.fn.magicmenu.defaults = {
		
		//flag for display state: hover | keystroke | always

		showMode: "hover",

		//F4 key = default keyCode for showMode === keystroke

		keyCode: 115,
		keyCodeString: "F4",

		//boolean flags for whether or not to construct the
		//HTML, plus the HTML templates to do so

		buildHTML: true,

		//CSS selectors for finding the elements of a magicmenu menu

		selectors: {
			outer: '.magicmenus',
			inner: '.magicmenusInner',
			all_lists: 'dl.magicmenu',
			list: 'dl.magicmenu_${domId}',
			header: 'dt',
			choice: 'dd',
			current: 'dd.${domId}_${current}'
		},

		//CSS selectors to be added and removed from elements during user interaction
		//These can be used as CSS hooks

		classes: {
			outer: '',
			invisibility: 'invisible',
			active: 'active',
			selected: 'selected'
		},

		//PURE directive for rendering HTML
		
		directive: {
			"dl": {
				"section<-context": {
					"@class+": "section.domId",
					"dt a": "section.label",
					"dt a@title": "click to select #{section.label}",
					"dd": {
						"choice<-section.choices": {
							"@class": "#{section.domId}_#{choice.value}",
							"a": "choice.text",
							"a@title": "click to select '#{choice.text}' as #{section.domId}"
						}
					}
				}
			}
		},

		defaultCallbacks: {
			before_init: function(){},
			after_init: function(){},
			header_before: function(){},
			header_after: function(){},
			choice_before: function(){},
			choice_after: function(){}
		},

		events: {

			//TODO - we shouldn't have to pass the fourth argument

			init: function( section, container, opts, getCurrentSelector ) {
				
				//style the initially selected element
				
				opts.events.choice_click.call(
					$( container ).find( getCurrentSelector( section ) ),
					section,
					container,
					opts
				);

			},
			header_click: function ( section, list, opts ) {

				//make the menu active
				$( list ).toggleClass( opts.classes.active );

			},

			//do DOM manipulations to set the state of the menu

			choice_click: function( section, list, opts ) {

				//remove active class from menu

				var $list = $( list )
					.toggleClass( opts.classes.active, false )
				;

				//turn previously selected back to normal

				var $previous = $list
					.find( opts.selectors.choice )
					.filter ( "." + opts.classes.selected )
					.removeClass( opts.classes.selected )
				;
				
				$previous
					.find( "a" )
					.attr( "title", function( i, attr ) {
						return attr.replace( "retain", "select" );
					} )
				;
				
				//transform newly selected
				
				var $current = $( this );

				$current
					.addClass( opts.classes.selected )
					.find( "a" )
					.attr( "title", function( i, attr ) {
						return attr.replace( "select", "retain" );
					} )
				;

				//set the .current property and, if necessary, set cookies

				//TODO - make this less coincidental
				var newVal = $current.attr( "class" );
				var newTheme = newVal.substring(
					( newVal.indexOf( "_" ) + 1 ),
					( newVal.indexOf( ' ' ) )
				);
				
				section.current = newTheme;
				
				if ( section.useCookies ) {

					//console.log( "setting cookie to " + newTheme );

					$.cookie( section.domId, newTheme, section.cookieOptions );
				}

			}

		}
	};

	/**
	 * A method for taking a compact, DRY object and creating a bunch of
	 * convenience properties so it can be used as a PURE context object
	 */
	$.fn.magicmenu.buildMenuSection = function( obj, callbacks, options, getCurrentFunction ) {
		
		if ( !obj ) {
			return null;
			//log an error
		}

		//build the initial object state

		var defaults = {
			useCookies: true,
			cookieOptions: {
				expires: 365
			}
		};
		
		var opts = $.extend( true, {}, defaults, options );
		var that = $.extend( true, {}, obj, opts );
		if ( callbacks ) {
			that.callbacks = callbacks;
		}
		
		//create a flattened array of possible values for this menu
		
		that.choiceValues = $.map( that.choices, function( choice ) {
			return choice.value
		} );
		
		//coerce label field from domId if necessary

		if ( that.label === undefined ) {
			that.label = that.domId;
		}

		//coerce text fields from value fields where necessary

		$.each( that.choices, function( i, choice ) {
			if ( choice.text === undefined ) {
				choice.text = choice.value;
			}
		} );
		
		//coerce a defaultIndex, then use it to set the default value

		if ( that.defaultIndex === undefined ) {
			that.defaultIndex = 0;
		}
		that.defaultValue = that.choiceValues[ that.defaultIndex ];
		
		//Look for a function or a cookie value to populate the
		//provisional current value
		
		that.current = null;

		if ( getCurrentFunction ) {
			that.current = getCurrentFunction();
		}
		if ( !that.current && that.useCookies ) {
			that.current = $.cookie( that.domId );
		}
		
		//Once you have a provisional current value, validate it.
		//If it's missing or invalid, replace it w/ the default.

		var validCurrent = (
			that.current &&
			( $.inArray( that.current, that.choiceValues ) >= 0 )
		);
		
		if ( !validCurrent ) {
			that.current = that.defaultValue;
		}

		if ( that.useCookies ) {
			$.cookie( that.domId, that.current, that.cookieOptions );
		}
		
		//add a function to get the complete current choice object
		that.getCurrentObject = function () {
			
			var obj = null;
			
			$.each( that.choices, function( i, n ) {
				if ( n.value === that.current ) {
					obj = n;
					return false;
				}
			} );

			return obj;
			
		};
		
		//return the built-out object

		return that;

	};	

} )( jQuery );
