/**
* FineTooth Utilitarian functions
** Copyright (c) 2006, Lindsey Simon <lsimon@commoner.com>
* All rights reserved.
* 
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
* 
* *       Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* *       Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* 
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**/

// disable the context menu for starters
document.oncontextmenu = function() { return false; }

/**
* So that console.log functions won't blow up IE or console.debug in Safari
* for firebug extension
*/
if ( typeof console == "undefined" || typeof console.debug == "undefined" ) { 
   console = {}; 
   console.log = console.debug = console.info = console.warn = console.error = function( msg ) {}; 
}


Object.extend(String.prototype, {
      
   trim: function() {
      var s = this;
      while ( s.substring(0,1) == ' ' || s.substring(0,1) == '\n' || s.substring(0,1) == '\t' || s.substring(0,1) == '\r') {
         s = s.substring( 1, s.length );
      }
      while (  s.substring( s.length - 1, s.length ) == ' ' || s.substring( s.length - 1, s.length ) == '\n' || s.substring( s.length - 1, s.length ) == '\t' || s.substring( s.length - 1, s.length ) == '\r' ) {
         s = s.substring( 0, s.length - 1 );
      }
      return s;
   }
   
});


Object.extend( Array.prototype, {
      
   unique: function( b ) {
      var a = [], i, l = this.length;
      for( i=0; i<l; i++ ) {
         if( a.indexOf( this[i], 0, b ) < 0 ) { a.push( this[i] ); }
      }
      return a;
   }
   
});


/****************************************************************************
* FineToothUtility 
****************************************************************************/
var FineToothUtility = Base.extend({
      
   // URL for ajax
   constructor: function() {
      this._AjaxOnExceptionCalledAlready = false;
      
      // timer for tick tock
      this._timer = [];
   },


   /**
    * Convert a string into an UTF-8 compactibale string, meaning the characters
    * in the string are supported by the UTF-8
    * @param {String} str The string to be converted
    * return the converted string
    */
   convertStringIntoUTFCompliant: function(str) {
      var converted = "";
      for ( var i=0; i < str.length; i++ ) {
         converted += (str.charCodeAt( i ) > 127) ? "&#"+str.charCodeAt( i )+";" : str.charAt(i);
      }
      return converted;
   },
   
   
   _UrlLastPiece: function() {
      return window.location.pathname.match( /\w+\.\w{3,4}$/ )[0];
   },
   
   /**
   * tagNamesToLowerCase
   * converts any tagNames in a string to lowercase
   * @param {string} xhtml
   * @return xhtml lowercased
   * @type {string}
   */
   tagNamesToLowerCase: function( xhtml, replaceArray ) {
      
      if ( parts = xhtml.match( /<([A-Z]+)/g ) ) {
         parts = parts.unique();
         for (var i = 0, n=parts.length; i < n; i++) {
            // strip off the start tag caret
            var part = parts[i].substring(1);
            
            xhtml = xhtml.replace(new RegExp( '<' + part, 'g'), '<' + part.toLowerCase() );
            xhtml = xhtml.replace(new RegExp( '<\/' + part, 'g'), '<\/' + part.toLowerCase() );
         }
      }
      return xhtml;
   },

   /**
   * serializeInnerToString
   * depends on Sarissa library from cross browser compatibililty
   * @param {object} DOM node
   * @param {array} additional text strings to strip out
   * @returns xhtml aka serialized innerHTML, with tagNames lower cased
   * @type {string}
   */
   serializeInnerToString: function( DOMNode, replaceArray ) {

      var xhtml = new XMLSerializer().serializeToString( DOMNode );
      var rootTagName = DOMNode.tagName;
      
      // now replace start and end tags
      var startTagRegexp = new RegExp( '<\/?' + rootTagName + '[^>]*?>', 'g' );
      xhtml = xhtml.replace( startTagRegexp, '' );
      
      return xhtml;
   },

   /**
   * Since onSuccess called before onComplete, we can test for errors here
   * @param {obj} transport
   * @param {obj} json
   */
   AjaxOnSuccess: function( transport, json ) {
      
      console.debug("FineToothUtility.AjaxOnSuccess transport:" + transport + ", json:" + json );
      
      
      // test for PHP Fatal Error and Warning
      var phpErrorMatch = new RegExp( "<b>(Warning|Fatal Error|Parse Error)</b>:", "i" );
      if ( transport.responseText.match( phpErrorMatch ) ) {
         throw { error_message: "PHP error, either Notice, Warning, or Fatal", error_details: transport.responseText, error_backtrace: null };
         return;
      }
      
      // test JSON for FineTooth FWExceptions
      if ( json && !json.success ) {
         // if we have an error_code or error_id from FWLoad, then json is an exception object
         if ( json.error_code  || json.error_id ) {
            throw json;
         }
      }
      
      // to just pop up an error message
      // HTMLForm.js has a catch for when json.success is false to just show the message
      
         
      // otherwise things are aok
      return true;     
   },

   /**
   * onFailure generic Ajax problem
   * @param {obj} transport
   * @param {obj} json
   */
   AjaxOnFailure: function( transport, json) {
      throw "FineToothUtility.AjaxOnFailure"; 
   },

   /**
   * onException Ajax
   * @param {obj} Ajax.Request
   * @param {obj} exception from prototype
   */
   AjaxOnException: function( request, exception ) {
   
      //alert( "FineToothUtility.AjaxOnException exception:" + exception + ", error_message: " + exception.error_message );
      //return;
      
      // stop exception from overwriting another loading href
      if ( this.onExceptionCalledAlready ) return;
      
      // prevent strung together js exceptions from overriding this one
      this.onExceptionCalledAlready = true;
      
      //console.log("FineToothUtility.AjaxOnException:" + exception); return;
      if ( exception.error_id ) {
         top.location.href = "?module=error&error_id=" + exception.error_id;
      }
      else {
         // fix for a FF bug that throws an exception if the listener is gone away && exception.name.match( /80040111/ )
         if ( exception && exception.message && exception.message.match( /80040111/ ) )
            return;
         
         
         var error_code = exception.error_code ? exception.error_code : "2033";
         var error_message = exception.error_message ? escape(exception.error_message) : escape( "Exception thrown during Ajax transaction" );
         
         // maybe we have details?
         // or else in IE we don't have a lot
         // in FF we get lineNumber and fileName
         var error_details = exception.error_details ? escape(exception.error_details).truncate( 500 ) : escape( exception.description ? exception.description : exception.message +", on line " + exception.lineNumber + " in file " + exception.fileName );
         var error_backtrace = exception.error_backtrace ? escape(exception.error_backtrace) : escape( "Parameters: " + request.options.parameters.truncate( 500 ) + '\nMethod: ' + request.options.method + '\nFFStack:' + exception.stack );
         var error_referrer = exception.error_referrer ? escape(exception.error_referrer) : "index.php";
         
         // error form
         top.location.href = "?module=error&error_code=" + error_code + "&error_message=" + error_message + "&error_details=" + error_details + "&error_backtrace=" + error_backtrace + "&error_referrer=" + error_referrer;
      }
   },

   /**
   * Simle AJAX requests for json success using prototype
   * param {obj} obj
   */
   AjaxRequest: function( obj ) {
      console.debug( "FineToothUtility.AjaxRequest parameters: " + obj.parameters );
      
      // we add on a parameter to break cache and so we know we're dealing with AJAX on the serverside
      var random_num = Math.round( ( Math.random() * 666 ) );
      
      // method
      var method = obj.method ? obj.method : "get";
      
      // if not specified url, then use our current url
      var url = obj.url ? obj.url : this._UrlLastPiece();
      
      new Ajax.Request( url, {
         asynchronous: obj.asynchronous ? obj.asynchronous : true,
         method: method,
         parameters: obj.parameters ? obj.parameters + "&AJAX=Ajax.Request_"+random_num : '',
         onLoading: obj.onLoading ? obj.onLoading : Prototype.emptyFunction,
         onSuccess: this.AjaxOnSuccess,
         onComplete: obj.onComplete,
         onFailure: this.AjaxOnFailure,
         onException: this.AjaxOnException
      } );
   },

   /**
   * Ajax updater stuffs innerHTML into a div with server returned xhtml 
   * param {element_id} element_id to be updated
   * param {obj} obj
   */
   AjaxUpdater: function( element_id, obj ) {
      //WG: Remove any ampersands from the beginning of the parameters list so that URL won't choke on certain servers
      if ( obj.parameters && obj.parameters.charAt(0) == '&' ) {
         obj.parameters = obj.parameters.substring( 1 );
      }
   	  
      console.debug( "FineToothUtility.AjaxUpdater element_id:" + element_id + ", parameters: " + obj.parameters );
      
      // we add on a parameter to both break cache and so we know we're dealing with AJAX serverside
      var random_num = Math.round( ( Math.random() * 666 ) );
      
      // method
      var method = obj.method ? obj.method : "get";
      
      // if not specified url, then use our current url
      var url = obj.url ? obj.url : this._UrlLastPiece();
      
      // do it
      new Ajax.Updater( element_id, url, {
         asynchronous: obj.asynchronous ? obj.asynchronous : true,
         method: method,
         parameters: obj.parameters + (obj.parameters == '' ? "AJAX=Ajax.Updater_" : "&AJAX=Ajax.Updater_") + random_num,
         onLoading: obj.onLoading ? obj.onLoading : Prototype.emptyFunction,
         onSuccess: this.AjaxOnSuccess,
         onComplete: this.AjaxUpdaterOnComplete.bind( this, element_id, obj.onComplete ),
         onFailure: this.AjaxOnFailure,
         onException: this.AjaxOnException,
         evalScripts: true
      });
      
   },

   /**
   * AjaxUpdaterOnComplete
   * allows us to capture all updaters and optionally perform onComplete
   * if the element to be updated is still in the renderable DOM
   * @param {string} element_id
   * @param {function} onComplete
   * @param {obj} transport
   * @param {obj} json
   */
   AjaxUpdaterOnComplete: function( element_id, onComplete, transport, json ) {
      console.debug( "FineToothUtility.AjaxUpdaterOnComplete element_id: " + element_id + ", transport: " + transport + ", json: " + json );
      
      // test to see if we should call oncomplete
      if ( onComplete && ( !element_id || typeof $( element_id ) !== "undefined" ) )
         ( onComplete )( transport, json );
   },
   
   /**
   * hide selects - temporarily disable any selects since their z-index is evil on IE
   * @param {string} element_id optional
   */
   hideSelects: function( element_id ) {
      if ( element_id )
         var scope = $( element_id );
      var selects = (scope || document).getElementsByTagName( 'select' );
      for (var i=0, n=selects.length; i<n; i++)
         selects[i].style.visibility = "hidden";	
   },

   /**
   * unhide selects 
   * @param {string} element_id optional
   */
   unhideSelects: function( element_id ) {
      console.debug( "FineToothUtility.unhideSelects" );
      // conditional for popup
      if ( !element_id && $( 'popupWrapper' ) && $( 'popupWrapper' ).visible() )
         return;
      
      if ( element_id )
         var scope = $( element_id );
      var selects = (scope || document).getElementsByTagName( 'select' );
      for (var i=0, n=selects.length; i<n; i++)
         selects[i].style.visibility = "visible";	
   },

   /**
   * Get an iframe document object - specific to browsers
   * @param {string} element_id
   * @return {obj} 
   */
   getIframeDocument: function( element_id ) {
      var iframe = $( element_id );
      var iframe_document;
      
      // For FF, etc ...
      if ( iframe.contentDocument )
         iframe_document = iframe.contentDocument; 
      // For IE5.5 and IE6
      else if (iframe.contentWindow)
         iframe_document = iframe.contentWindow.document;
      // For IE5
      else if (iframe.document)
         iframe_document = iframe.document;
      return iframe_document;
   },

   /**
   * Dynamically Load a JavaScript file one time into memory
   */
   loadJavaScriptFileOnce: function( filename ) {
      var scripts = document.getElementsByTagName( 'script' );
      var loaded = $A( scripts ).find( function( element ) {
         return element.src.match( filename );
      });
      if ( loaded ) return;
      
      var script = document.createElement( 'script' );
      script.type = 'text/javascript';
      script.src = filename; 
      document.getElementsByTagName( 'head' )[0].appendChild( script );
   },

   /**
   * tick
   * @param {string} caller function
   */
   tick: function( caller ) {
      this._timer[caller] = {};
      this._timer[caller].start = new Date();
   },

   /**
   * tock
   * @param {string} caller function
   */
   tock: function( caller ) {
      this._timer[caller].end = new Date();
      console.info( caller + ' took: '  + ( this._timer[caller].end  - this._timer[caller].start ) + ' ms.' );   
   }
});

// store in global namespace
Utility = new FineToothUtility();

/****************************************************************************
Modifications to Prototype / Scriptaculous
****************************************************************************/

// Fixes the scroll noticing in prototype for Sortables
Position.includeScrollOffsets = true;

/**
* For some reason prototype doesn't have this
* probably because it's unreliable or gheigh
*/
Object.extend(Event, {	
   isRightClick: function(event) {
      return (((event.which) && (event.which == 3)) || ((event.button) && (event.button == 2)));
   }
});


/**
* My own version of serializing the Sortable
* @param {string} element
*/
Sortable.serializeToString = function(element) {
   element = $(element);
   var options = Object.extend(Sortable.options(element), arguments[1] || {});
   var name = encodeURIComponent(
      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
   
   return Sortable.sequence(element, arguments[1]).map( function(item) {
      return encodeURIComponent(item);
   }).join(',');
}


/****************************************************************************
JavaSCript XPATH - http://blog.km0ti0n.be/js/MozXPath/
***************************************************************************/
if( typeof XMLDocument == "undefined" )
   XMLDocument = Class.create();

if( document.implementation.hasFeature("XPath", "3.0") && !XMLDocument.prototype.selectNodes ) { 

   // prototying the XMLDocument 
   XMLDocument.prototype.selectNodes = function(cXPathString, xNode) 
   { 
      if( !xNode ) { xNode = this; }
      var oNSResolver = this.createNSResolver(this.documentElement) 
      var aItems = this.evaluate(cXPathString, xNode, oNSResolver,
         XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null) 
      var aResult = [];
      for( var i = 0; i < aItems.snapshotLength; i++) 
      { 
         aResult[i] = aItems.snapshotItem(i);
      } 
      return aResult;
   }
   // prototying the XMLDocument 
   XMLDocument.prototype.selectSingleNode = function(cXPathString, xNode) 
   { 
      if( !xNode ) { xNode = this; }
      var xItems = this.selectNodes(cXPathString, xNode);
      if( xItems.length > 0 ) 
      { 
         return xItems[0];
      } 
      else 
      { 
         return null;
      } 
   } 
   
}

/****************************************************************************
EventPublisher for custom events
****************************************************************************/
/**
 * The EventPublisher class allows objects to fire events (and other objects to
 * subscribe handlers to those events) and may pass optional arguments.
 * by Ryan Gahl <ryan.gahl@gmail.com>
 * You are free to use the code for commercial or any other purpose, and you can
 * modify it as you see fit. But please do not re-distribute the code or any
 * derivative without including my name in some form in the credits. 
 */ 
EventPublisher = Class.create();
EventPublisher.prototype = 
{	
	/**
	 * @constructor
	 */
	initialize: function() 
	{
	},
	
	/**
	 * Attaches a {handler} function to the publisher's {eventName} event for execution upon the event firing
	 * @param {String} eventName The name of the event to attach the handler function to
	 * @param {Function} handler The function which will execute when the specified event is fired
	 * @param {Boolean} asynchFlag [optional] Defaults to false if omitted. Indicates whether to execute {handler} asynchronously (true) or not (false).
	 */ 
	attachEventHandler: function(eventName, handler) 
	{
		// using an event cache array to track all handlers for proper cleanup
		if (this.allEvents == null)
			this.allEvents = new Array();
		// loop through the event cache to prevent adding duplicates
		var len = this.allEvents.length;
		var foundEvent = false;
		for (var i = 0; i < len; i++) 
		{
			if (this.allEvents[i] == eventName) 
			{
				foundEvent = true;
				break;
			}
		}
		if (!foundEvent)
			this.allEvents.push(eventName);
			
		eventName = eventName + "_evt"; // appending _evt to event name to avoid collisions
		if (this[eventName] == null)
			this[eventName] = new Array();
			
		//create a custom object containing the handler method and the asynch flag
		var asynchVar = arguments.length > 2 ? arguments[2] : false;
		var handlerObj = 
		{
			method: handler,
			asynch: asynchVar
		};
		
		this[eventName].push(handlerObj);
	},
	
	/**
	 * Removes a single handler from a specific event
	 * @param {String} eventName The event name to clear the handler from
	 * @param {Function} handler A reference to the handler function to un-register from the event
	 */ 
	removeEventHandler: function(eventName, handler) 
	{
		eventName = eventName + "_evt"; // appending _evt to event name to avoid collisions
		if (this[eventName] != null)
			this[eventName] = this[eventName].reject(function(obj) { return obj.method == handler; });
	},
	
	/**
	 * Removes all handlers from a single event
	 * @param {String} eventName The event name to clear handlers from
	 */ 
	clearEventHandlers: function(eventName) 
	{
		eventName = eventName + "_evt"; // appending _evt to event name to avoid collisions
		this[eventName] = null;
	},
	
	/**
	 * Removes all handlers from ALL events
	 */ 
	clearAllEventHandlers: function() 
	{
		if (this.allEvents) 
		{
			var len = this.allEvents.length;
			for (var i = 0; i < len; i++) 
			{
				this.clearEventHandlers(this.allEvents[i]);
			}
		}
	},
   
	/**
	 * Fires the event {eventName}, resulting in all registered handlers to be executed.
	 * @param {String} eventName The name of the event to fire
	 * @params {Object} args [optional] Any object, will be passed into the handler function as the only argument
	 */
	fireEvent: function(eventName) 
	{
		var evtName = eventName + "_evt"; // appending _evt to event name to avoid collisions
		if (this[evtName] != null) 
		{
			var len = this[evtName].length; //optimization
							
			for (var i = 0; i < len; i++)
			{
				try
				{
					if (arguments.length > 1)
					{
						if (this[evtName][i].asynch)
						{
							var eventArgs = arguments[1];
							var method = this[evtName][i].method.bind(this);
							setTimeout(function() { method(eventArgs) }.bind(this), 10);
						}
						else
							this[evtName][i].method(arguments[1]);                        
					} else 
					{
						if (this[evtName][i].asynch)
						{
							var eventHandler = this[evtName][i].method;
							setTimeout(eventHandler, 1);
						}
						else
							if (this && this[evtName] && this[evtName][i] && this[evtName][i].method)
								this[evtName][i].method();
					}
				}
				catch (e) 
				{
					if (this.id) 
					{
						alert("error: error in " + this.id + ".fireevent():\n\nevent name: " + eventName + "\n\nerror message: " + e.message);
					} 
					else 
					{
						alert("error: error in [unknown object].fireevent():\n\nevent name: " + eventName + "\n\nerror message: " + e.message);
					}
				}
			}
		}
	}
};