/*************************************************************
	Released under the GNU General Public License

	The following copyright announcement is in compliance
	to section 2c of the GNU General Public License, and
	thus can not be removed, or can only be modified
	appropriately.

	Please leave this comment intact together with the
	following copyright announcement.

	Copyright(c) 2010 Allies Computing Ltd

	The authors provide no warranty.

	World Addresses Web Example Version 1.00 Beta 1
	By Allies Computing Ltd - www.allies-computing.co.uk
*************************************************************/

/**
*	Class: WorldAddresses
*
*	Example:
*
*		(start code)
*
*		new WorldAddresses({
*			searchURL: "example.php",
*			searchKey: "XXXXX-XXXXX-XXXXX-XXXXX",
*			functionName: "WA_G2TPC",
*			dataset: "ALL",
*			searchField: "searchvalue",
*			searchButton: "searchbutton",
*			paging: true,
*			outputMap: {
*				fields: [{
*					id: "address1",
*					dataIndex: 0
*				},{
*					id: "address2",
*					dataIndex: 1
*				},{
*					id: "town",
*					dataIndex: 2
*				},{
*					id: "postcode",
*					dataIndex: 3
*				},{
*					id: "country",
*					dataIndex: 4
*				}]
*			}
*		});
*
*		(end)
*/

var WorldAddresses = Class.create({
    version: "1.01",

    /**
	*   Constructor: initialize
	*
	*	Initializes the object
	*
	*	Parameters:
	*
	*		config.searchKey - World Addresses search key. (Required)
	*		config.functionName - World Addresses function name. (Required)
	*		config.dataset - World Addresses dataset. (Required)
	*		config.datasetField - Element ID of the select field to update the dataset (Optional)
	*		config.searchURL - Path to the server side script. If passed a remote URL will call jsonpRequest, otherwise will call proxyRequest. (Required)
	*		config.searchField - Element ID of the field to get the search value from. (Optional if using inputMap)
	*		config.searchButton - Element ID of the button which will trigger the search. (Required)
	*		config.paging - If there are more than 100 results for a country display an option to view more results. (Optional)
	*		config.inputMap - Allows user to specify input parameters and their order. (Optional)
	*		config.outputMap (Required)
	*			config.outputMap.fields - Allows user to map output from the service to form elements.
	*				config.outputMap.fields.id - Element ID of the field you would like to map data to.
	*				config.outputMap.fields.dataIndex - The index of the data you would like to map to the form element.
	*			config.outputMap.selectItems - Specify indices of the data you would like to display on the select address dropdown.
	*		config.outputMap.appendText - The string you would like to append the data together with.
	*		config.debug - If set to true the class will either show debug information in console or alerts. (Optional)
	*
	*	Returns:
	*
	*		Instance of this class
	*/
    initialize: function(config){
        config = config || {};

        if(typeof(config.debug) != "undefined" && config.debug == true){
            if(typeof(window.WorldAddressesDebug) != "undefined" || typeof(window.console) != "undefined"){
                this.debug("WARNING - With debug mode enabled you may experience a slight decrease in performance. Make sure to frequently clear your browser's cache and reload the page during testing.<br />");
                this.debug("WorldAddresses->initialize");
            }
        }

        this.config = {};

        //Apply user config to default config
        Object.extend(this.config, config);

        //Set initial values
        this.searchField;
        this.addresses = new Array();
        this.counts = new Array();
        this.initialRequest = true;
        this.currentPage = 0;
        this.numPages = 0;
        this.lastIndex = 0;

        if(this.validateConfig()){
            this.initEvents();
        } else {
            if(this.config.searchButton && typeof(this.config.searchButton) == "string"){
                if($(this.config.searchButton)){
                    $(this.config.searchButton).setAttribute("disabled", "disabled");
                }
            }
        }

        return this;
    },

    validateConfig: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->validateConfig");
        }

        if(typeof(this.config.searchKey) == "undefined" || this.config.searchKey == ""){
            this.displayError(this.getString("error001"));
            return false;
        }

        if(!/^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/i.test(this.config.searchKey)){
            this.displayError(this.getString("error002"));
            return false;
        }

        if(typeof(this.config.functionName) == "undefined" || this.config.functionName == ""){
            this.displayError(this.getString("error003"));
            return false;
        }

        if(!/^[A-Z0-9-_]+$/i.test(this.config.functionName)){
            this.displayError(this.getString("error004"));
            return false;
        }

        if(typeof(this.config.dataset) == "undefined" || this.config.dataset == ""){
            this.displayError(this.getString("error005"));
            return false;
        }

        if(this.config.datasetField && this.config.datasetField != ""){
            if(typeof(this.config.datasetField) != "string"){
                this.displayError(this.getString("error007"));
                return false;
            }

            if(!$(this.config.datasetField)){
                this.displayError(this.getString("error008", "\"" + this.config.datasetField + "\""));
                return false;
            }

			this.config.dataset = $(this.config.datasetField).getValue();
        }

        if(typeof(this.config.searchField) == "undefined" || this.config.searchField == ""){
            this.displayError(this.getString("error009"));
            return false;
        }

        if(typeof(this.config.searchField) != "string"){
            this.displayError(this.getString("error010"));
            return false;
        }

        if(!$(this.config.searchField)){
            this.displayError(this.getString("error011", "\"" + this.config.searchField + "\""));
            return false;
        }

        if(typeof(this.config.searchButton) == "undefined" || this.config.searchButton == ""){
            this.displayError(this.getString("error012"));
            return false;
        }

        if(typeof(this.config.searchButton) != "string"){
            this.displayError(this.getString("error013"));
            return false;
        }

        if(!$(this.config.searchButton)){
            this.displayError(this.getString("error014", "\"" + this.config.searchButton + "\""));
            return false;
        }

        if(this.config.paging){
            if(typeof(this.config.paging) != "boolean"){
                this.displayError(this.getString("error015"));
                return false;
            }
        }

        if(typeof(this.config.outputMap) == "undefined"){
            this.displayError(this.getString("error016"));
            return false;
        }

        if(typeof(this.config.outputMap.fields) == "undefined"){
            this.displayError(this.getString("error017"));
            return false;
        }

        if(typeof(this.config.outputMap.fields) != "object" || typeof(this.config.outputMap.fields.length) == "undefined"){
            this.displayError(this.getString("error018"));
            return false;
        }

        if(this.config.outputMap.fields.length == 0){
            this.displayError(this.getString("error019"));
            return false;
        }

        var length = this.config.outputMap.fields.length;

        for(var i = 0; i < length; i++){
            var objFieldMap = this.config.outputMap.fields[i];

            if(typeof(objFieldMap.id) == "undefined" || objFieldMap.id == ""){
                this.displayError(this.getString("error020", i));
                return false;
            }

            if(typeof(objFieldMap.id) != "string"){
                this.displayError(this.getString("error021", i));
                return false;
            }

            if(!$(objFieldMap.id)){
                this.displayError(this.getString("error022", i, "\"" + objFieldMap.id + "\""));
                return false;
            }

            if(typeof(objFieldMap.dataIndex) == "undefined"){
                this.displayError(this.getString("error023", i));
                return false;
            }

            if(typeof(objFieldMap.dataIndex) != "number" && (typeof(objFieldMap.dataIndex) != "object" || typeof(objFieldMap.dataIndex.length) == "undefined")){
                this.displayError(this.getString("error024"));
                return false;
            }
        }

        if(typeof(this.config.debug) != "undefined" && this.config.debug == true){
            if(typeof(window.WorldAddressesDebug) == "undefined" && typeof(window.console) == "undefined"){
                this.displayError(this.getString("error025"));
            }
        }

        return true;
    },

    /**
	*  Function: initEvents
	*
	*  Setup event listeners.
	*/
    initEvents: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->initEvents");
        }

        //Set the onClick event handler for the defined search button
        $(this.config.searchButton).observe('click', this.initSearch.bind(this));

        //If searchField is defined
        if(this.config.searchField){
            this.searchField = $(this.config.searchField);

            //Simulate a click when the enter key is pressed
            $(this.searchField).observe('keypress', function(event){
                if(event.keyCode == 13){
                    $(this.config.searchButton).click();
                }
            }.bind(this));
        } else if(this.config.inputMap){
            //If inputMap is defined, look for a fieldType of "searchValue".
            this.config.inputMap.each(function(obj){
                if(obj.fieldType && obj.fieldType == "searchValue"){
                    this.searchField = $(obj.id);

                    $(this.searchField).observe('keypress', function(event){
                        if(event.keyCode == 13){
                            $(this.config.searchButton).click();
                        }
                    }.bind(this));
                }
            }.bind(this));
        }

        //Set the onChange event handler for the dataset combo
        if(this.config.datasetField){
            $(this.config.datasetField).observe("change", this.onChangeDataset.bind(this));
        }
    },

    /**
	*  Function: initSearch
	*
	*  This function attempts to search for a match. If successful it will display a list of matches.
	*/
    initSearch: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->initSearch");
        }

        //Set initial values
        this.addresses = new Array();
        this.counts = new Array();
        this.initialRequest = true;
        this.currentPage = 0;

        //Clear all fields
        this.clearFields();

        this.btnSearchValue = $(this.config.searchButton).getValue();

        $(this.config.searchButton).setValue(this.getString("searchButtonSearchingText") + "...");

        this.searchValue = this.trim($(this.searchField).getValue());

        if(this.searchValue !== false && this.searchValue != ""){
            //If the URL is remote then make a request using jsonP, else make a request through a script on the current server
            if(this.isRemoteURL(this.config.searchURL)){
                this.searchValue = encodeURIComponent(this.searchValue);

                this.jsonpRequest();
            } else {
                this.proxyRequest();
            }
        } else {
            //Please provide a search value.
            this.displayWarning(this.getString("warning001"));

            $(this.config.searchButton).setValue(this.btnSearchValue);
        }
    },

    /**
	*  Function: showMoreResults
	*
	*  This function attempts to search for the next page of results. If successful it will display a list of matches.
	*/
    showMoreResults: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->showMoreResults");
        }

        //Increase page count and flag as not being the first call in this line of searches
        this.currentPage += 1;
        this.initialRequest = false;

        if(this.config.debug == true){
            this.debug("Fetch page " + parseInt(this.currentPage + 1) + " of results");
        }

        if(this.isRemoteURL(this.config.searchURL)){
            this.jsonpRequest();
        } else {
            this.proxyRequest();
        }
    },

    /**
	*	Function: jsonpRequest
	*
	*	This function attempts to make a JSONP request directly to the World Addresses server.
	*/
    jsonpRequest: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->jsonpRequest");
        }

        //Build URL
        var url = this.config.searchURL;
        url += "/" + this.config.searchKey;
        url += "/" + this.config.functionName;
        url += "/" + this.config.dataset;
        url += "/" + this.buildInputParams();
        url += "/page/" + this.currentPage;
        url += "/json";

        if(this.config.debug == true){
            this.debug("Making request to " + url);
        }

        //Perform request
        new Ajax.JSONRequest(url, {
            method: 'post',
            callbackParamName: "method",
            onSuccess: this.requestOnSuccess.bind(this),
            onFailure: this.requestOnFailure.bind(this)
        });
    },

    /**
	*  Function: proxyRequest
	*
	*  This function attempts to make a request via a proxy.
	*/
    proxyRequest: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->proxyRequest");
        }

        var params = {
            searchKey: this.config.searchKey,
            functionName: this.config.functionName,
            dataset: this.config.dataset,
            searchValue: this.buildInputParams(),
            page: this.currentPage
        };

        if(this.config.debug == true){
            this.debug("Making request to " + this.config.searchURL);
            this.debug("Params:");
            this.debug(params);
        }

        //Perform request
        new Ajax.Request(this.config.searchURL, {
            method: 'post',
            parameters: params,
            onSuccess: this.requestOnSuccess.bind(this),
            onFailure: this.requestOnFailure.bind(this)
        });
    },

    /**
	*	Function: requestOnSuccess
	*
	*	This function is called on a successful request.
	*
	*	Parameters:
	*
	*		transport - the response object
	*/
    requestOnSuccess: function(transport){
        if(this.config.debug == true){
            this.debug("WorldAddresses->requestOnSuccess");
        }

        if(transport.responseText != ""){
            var response = transport.responseText.evalJSON();

            if(this.config.debug == true){
                this.debug(response);
            }

            if(typeof(response) == "undefined"){
                //The server failed to respond
                this.displayError(this.getString("error100"));

                $(this.config.searchButton).setValue(this.btnSearchValue);

                return false;
            }

            //Check if the reponse is ok and contains the info we need
            if(typeof(response) == "object"){
                if(response.Status){
                    if(response.Status == "Ok"){
                        if(response.Addresses){
                            if(response.Addresses.length > 0){
                                this.lastIndex = this.addresses.length;

                                //If this is the initial request set the results else append them
                                if(this.initialRequest == true){
                                    this.addresses = response.Addresses;
                                    this.counts = response.Counts;
                                } else {
                                    response.Addresses.each(function(addressLine){
                                        this.addresses.push(addressLine);
                                    }.bind(this));
                                }

                                //if there's only one address populate the form else display/update the select box
                                if(this.addresses.length == 1){
                                    this.populateFields();
                                } else {
                                    if(this.initialRequest == true){
                                        this.displaySelection();
                                    } else {
                                        this.updateSelection();
                                    }
                                }
                            } else {
                                //No results found
                                this.displayWarning(this.getString("warning002"));
                            }
                        } else {
                            //No results found
                            this.displayWarning(this.getString("warning002"));
                        }
                    } else if(typeof(response.Status) == "string"){
                        //error message from service
                        this.displayError(response.Status);
                    }
                } else {
                    //There was an unknown problem
                    this.displayError(this.getString("error105"));
                }
            } else {
                //There was an unknown problem
                this.displayError(this.getString("error105"));
            }
        } else {
            //The server failed to respond.
            this.displayError(this.getString("error100"));
        }

        $(this.config.searchButton).setValue(this.btnSearchValue);
    },

    /**
	*  Function: requestOnFailure
	*
	*  This function is called on a failed request.
	*
	*	Parameters:
	*
	*		response - the response object
	*/
    requestOnFailure: function(response){
        if(this.config.debug == true){
            this.debug("WorldAddresses->requestOnFailure");
            this.debug("status:\n" + response.status);
            this.debug("statusText:\n" + response.statusText);
            this.debug("responseText:\n" + response.responseText);
        }

        switch(response.status){
            case 404:
                //The page you have requested could not be found.
                this.displayError(this.getString("error101"));
                break;
            case 500:
                //There was an internal server error.
                this.displayError(this.getString("error102"));
                break;
            case 504:
                //The service didn't respond.
                this.displayError(this.getString("error103"));
                break;
            default:
                //There was a problem with the request.
                this.displayError(this.getString("error104"));
        }

        $(this.config.searchButton).setValue(this.btnSearchValue);
    },

    /**
	*  Function: displaySelection
	*
	*  This function builds and displays a list of matches.
	*/
    displaySelection: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->displaySelection");
        }

        var html = "<div id=\"waExampleSelection\"><ul><li><ul id=\"waExampleSelectionInner\">";

        this.addresses.each(function(address, i){
            var addressLine = new Array();

            if(this.config.outputMap.selectItems){
                this.config.outputMap.selectItems.each(function(index){
                    if(address[index] && address[index] != ""){
                        addressLine.push(address[index]);
                    }
                }.bind(this));
            } else {
                address.each(function(el){
                    if(el != ""){
                        addressLine.push(el);
                    }
                }.bind(this));
            }

            html += "<li><a class=\"address_option\" href=\"javascript:void(0);\" rel=\"" + i + "\">" + addressLine.join(", ") + "</a></li>";
        }.bind(this));

        //If paging is on, set initial values and display a "Show more results" button
        if(this.config.paging === true){
            var highestTotal = 0;
            var numPages = 0;

            $H(this.counts).each(function(obj){
                var country = obj[0];
                var onPage = obj[1].OnPage;
                var totalCount = obj[1].TotalCount;

                if(totalCount == onPage){
                    numPages = 0;
                }

                if(totalCount > onPage){
                    numPages = Math.ceil(totalCount / onPage) - 1;
                }

                if(numPages >= this.numPages){
                    this.numPages = numPages;
                }
            }.bind(this));

            if(this.numPages > 0){
                html += "<li><a id=\"show_more\" href=\"javascript:void(0);\">" + this.getString("showMoreResultsText") + "...</a></li>";
            }
        }

        html += "</ul></li></ul></div>";

        Modalbox.show(html, {
            title: this.getString("selectionTitle"), //"Please select your address"
            height: 350,
            width: 600,
			displayTag: this.config.displayTag,
            afterLoad: function(){
                $$(".address_option").each(function(el){
                    $(el).observe('click', this.populateFields.bind(this));
                }.bind(this));

                if(this.config.paging === true && this.numPages > 0){
                    $("show_more").observe("click", this.showMoreResults.bind(this));
                }
            }.bind(this)
        });
    },

    /**
	*  Function: updateSelection
	*
	*  This function updates the list of matches.
	*/
    updateSelection: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->updateSelection");
        }

        var html = "";

        //Update select box with more results
        for(var i = this.lastIndex; i < this.addresses.length; i++){
            var addressLine = new Array();
            var address = this.addresses[i];

            if(this.config.outputMap.selectItems){
                this.config.outputMap.selectItems.each(function(index){
                    if(address[index] && address[index] != ""){
                        addressLine.push(address[index]);
                    }
                }.bind(this));
            } else {
                address.each(function(el){
                    if(el != ""){
                        addressLine.push(el);
                    }
                }.bind(this));
            }

            html += "<li><a class=\"address_option\" href=\"javascript:void(0);\" rel=\"" + i + "\">" + addressLine.join(", ") + "</a></li>";
        }

        $("waExampleSelectionInner").insert(html);

        $$(".address_option").each(function(el){
            $(el).observe('click', this.populateFields.bind(this));
        }.bind(this));

        //If there are more results available, display another "Show more results" button
        if(this.currentPage < this.numPages){
            var show_more = $("show_more").remove();
            $("waExampleSelectionInner").insert(show_more);
        } else {
            $("show_more").remove();
        }
    },

    /**
	*  Function: hideSelection
	*
	*  This function hides the list of matches if it exists.
	*/
    hideSelection: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->hideSelection");
        }

        if(Modalbox.initialized){
            Modalbox.hide();
        }
    },

    /**
	*	Function: populateFields
	*
	*	This function populates the fields according to the Id's provided in the outputMap.
	*
	*	Parameters:
	*
	*		e - the event object
	*/
    populateFields: function(e){
        if(this.config.debug == true){
            this.debug("WorldAddresses->populateFields");
        }

        var indexAddress = 0;

        if(e){
            indexAddress = $(Event.element(e)).rel;
        }

        var selectedAddress = this.addresses[indexAddress];

        this.config.outputMap.fields.each(function(field){
            if(field.id && field.id != ""){
                if(typeof(field.dataIndex) != "undefined"){
                    if (typeof(field.dataIndex) == "number"){
                        $(field.id).setValue(selectedAddress[field.dataIndex]);
                    } else if (typeof(field.dataIndex) == "object"){
                        var addressLine = [];

                        field.dataIndex.each(function(dataIndex){
                            if(selectedAddress[dataIndex] && selectedAddress[dataIndex] != ""){
                                addressLine.push(selectedAddress[dataIndex]);
                            }
                        }.bind(this));

                        $(field.id).setValue(addressLine.join(this.config.outputMap.appendText));
                    }
                }
            }
        }.bind(this));

        this.hideSelection();
    },

    /**
	*  Function: buildInputParams
	*
	*  This function builds the inputParam string from the searchField and the inputMap config items.
	*
	*/
    buildInputParams: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->buildInputParams");
        }

        var inputParams = "";
        var joiner = "";

        switch(true){
            case typeof(this.config.searchField) == "string" && typeof(this.config.inputMap) != "object":
                inputParams = this.searchValue;
                break;
            case typeof(this.config.searchField) == "string" && typeof(this.config.inputMap) == "object":
                //searchfield is defined as well as inputmap. Assume searchfield's value is first parameter
                inputParams = this.searchValue;

                this.config.inputMap.each(function(obj){
                    inputParams += "/" + encodeURIComponent($(obj.id).getValue());
                }.bind(this));

                break;
            case typeof(this.config.searchField) != "string" && typeof(this.config.inputMap) == "object":
                this.config.inputMap.each(function(obj){
                    switch(true){
                        case obj.fieldType && obj.fieldType == "searchValue":
                            inputParams += joiner + this.searchValue;
                            break;
                        default:
                            inputParams += joiner + encodeURIComponent($(obj.id).getValue());
                            break;
                    }
                    joiner = "/";
                }.bind(this));

                break;
        }

        return inputParams;
    },

    /**
	*  Function: onChangeDataset
	*
	*  This function updates the dataset value.
	*
	*/
    onChangeDataset: function(){
        this.setDataset($(this.config.datasetField).getValue());
    },

    /**
	*  Function: setDataset
	*
	*  This function updates the dataset value.
	*
	*	Parameters:
	*
	*		value - A string value ("ALL", "EU", "UK", "US", "FR", "DE")
	*/
    setDataset: function(value){
        if(value != "" && typeof(value) == "string"){
            this.config.dataset = value;
        }
    },

    /**
	*	Function: isRemoteURL
	*
	*	Checks if the URL is remote
	*
	*	Parameters:
	*
	*		url - The URL
	*
	*	Returns:
	*
	*		Boolean
	*/
    isRemoteURL: function(url){
        if(this.config.debug == true){
            this.debug("WorldAddresses->isRemoteURL");
        }

        if(url.indexOf("http://") == -1){
            return false;
        }

        url = this.parseURL(url, 'PHP_URL_HOST');

        return !(url == window.location.host);
    },

    /**
	*	Function: clearFields
	*
	*	Clears field values set in the outputMap config.
	*
	*/
    clearFields: function(){
        if(this.config.debug == true){
            this.debug("WorldAddresses->clearFields");
        }

        this.config.outputMap.fields.each(function(field){
            $(field.id).setValue("");
        }.bind(this));
    },

    /**
	*	Function: getString
	*
	*	Check for the existence of language string
	*
	*	Parameters:
	*
	*		s - the placeholder name
	*
	*	Returns:
	*
	*		The string loaded from the language file or the placeholder name
	*/
    getString: function(s) {
        if(typeof(window.WorldAddressesConfig) != "undefined"){
            if(typeof(WorldAddressesConfig.lang) != 'undefined' && WorldAddressesConfig.lang[s]){
                s = WorldAddressesConfig.lang[s];
            }
        }

        for (var i=1; i < arguments.length; i++){
            s = s.replace(/\{\d+?\}/, arguments[i]);
        }

        return s;
    },

    /**
	*  	Function: trim
	*
	*  	Removes whitespace from the beginning and end of string.
	*
	*  	Parameters:
	*
	*		value - the value you want to trim
	*
	*	Returns:
	*
	*		The trimmed string
	*/
    trim: function(value){
        if(!value || typeof(value) != "string"){
            return "";
        }
        return value.replace(/^\s+|\s+$/g,"");
    },

    /**
	*	Function: parseURL
	*
	*	Parse a URL and return it's components
	*
	*	Parameters:
	*
	*		str - the url
	*		component - determines which part of the URL you want to be returned
	*
	*	Returns:
	*
	*		The part of the url requested
	*
	*	Source:
	*
	*		<http://phpjs.org>
	*/
    parseURL:function (str, component) {
        var  o   = {
            strictMode: false,
            key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
            q:   {
                name:   "queryKey",
                parser: /(?:^|&)([^&=]*)=?([^&]*)/g
            },
            parser: {
                strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
                loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/\/?)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ // Added one optional slash to post-protocol to catch file:/// (should restrict this)
            }
        };

        var m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
        uri = {},
        i   = 14;
        while (i--) {
            uri[o.key[i]] = m[i] || "";
        }

        switch (component) {
            case 'PHP_URL_SCHEME':
                return uri.protocol;
            case 'PHP_URL_HOST':
                return uri.host;
            case 'PHP_URL_PORT':
                return uri.port;
            case 'PHP_URL_USER':
                return uri.user;
            case 'PHP_URL_PASS':
                return uri.password;
            case 'PHP_URL_PATH':
                return uri.path;
            case 'PHP_URL_QUERY':
                return uri.query;
            case 'PHP_URL_FRAGMENT':
                return uri.anchor;
            default:
                var retArr = {};
                if (uri.protocol !== '') {
                    retArr.scheme=uri.protocol;
                }
                if (uri.host !== '') {
                    retArr.host=uri.host;
                }
                if (uri.port !== '') {
                    retArr.port=uri.port;
                }
                if (uri.user !== '') {
                    retArr.user=uri.user;
                }
                if (uri.password !== '') {
                    retArr.pass=uri.password;
                }
                if (uri.path !== '') {
                    retArr.path=uri.path;
                }
                if (uri.query !== '') {
                    retArr.query=uri.query;
                }
                if (uri.anchor !== '') {
                    retArr.fragment=uri.anchor;
                }
                return retArr;
        }
    },

    /**
	*	Function: displayWarning
	*
	*	Displays a warning to the user.
	*
	*	Parameters:
	*
	*		message - the message you would like to display
	*/
    displayWarning: function(message){
        if(this.config.debug == true){
            this.debug(message);
        }

        Modalbox.show("<div class=\"waError\">" + message + "</div>", {
            title: this.getString("warningTitle"),
			displayTag: this.config.displayTag,
            width: 600
        });
    },

    /**
	*	Function: displayError
	*
	*	Displays an error to the user. If debug mode is false then the function displays a default error message.
	*
	*	Parameters:
	*
	*		message - the message you would like to display
	*/
    displayError: function(message){
        if(this.config.debug == true){
            this.debug(message);
        } else {
            //There was a problem with the service. Please enter your address manually.
            message = this.getString("errorDefault");
        }

        Modalbox.show("<div class=\"waError\">" + message + "</div>", {
            title: this.getString("errorTitle"),
			displayTag: this.config.displayTag,
            width: 600
        });
    },

    /**
	*  	Function: debug
	*
	*  	Displays a debug message. Useful for developers trying to track down errors.
	*
	*  	Parameters:
	*
	*		message - the message you would like to display
	*/
    debug: function(message){
        if(typeof(window.WorldAddressesDebug) != "undefined"){
            if (typeof(this.waDebug) == "undefined"){
                this.waDebug = new WorldAddressesDebug();
            }

            this.waDebug.addMessage(message);
        } else if(window.console) {
            console.info(message);
        }
    }
});

/***********************************************************************************
************************************************************************************
***********************************************************************************/

/**
*	Class: WorldAddressesDebug
*
*	Class to display a small window which updates with debug information
*/
var WorldAddressesDebug = Class.create({
    version: "1.00 Beta 1",

    /**
	*   Constructor: initialize
	*
	*	Initializes the object
	*
	*	Parameters:
	*
	*  		config.debug - If set to true the class will either show debug information in console or alerts.
	*/
    initialize: function(config){
        config = config || {};

        if(config.debug == true){
            this.debug("WorldAddressesDebug->initComponent");
        }

        this.config = {};

        if(config) {
            Object.extend(this.config, config);
        }

        if(this.config.debug == true){
            this.debug("Initial Config:");
            this.debug(this.config);
        }

        this.messages = [];

        this.createWindow();

        return this;
    },

    /**
	*   Function: createWindow
	*
	*	Create a window
	*/
    createWindow: function(){
        this.dbgWindow = new Element("div", {
            id: "waExampleDebugWindow"
        });
        this.dbgTitleBar = new Element("div", {
            id: "waExampleDebugTitleBar"
        });
        this.dbgContent = new Element("div", {
            id: "waExampleDebugContent"
        });

        new Draggable(this.dbgWindow, {
            handle: this.dbgTitleBar
        });

        this.dbgTitleBar.update("World Addresses Debug Information");

        this.dbgWindow.insert({
            "top": this.dbgTitleBar
            });
        this.dbgWindow.insert({
            "bottom": this.dbgContent
            });
        this.dbgContent.insert({
            "top": this.dbgContentInner
            });
        $(document.body).insert({
            "bottom": this.dbgWindow
            });
    },

    /**
	*   Function: addMessage
	*
	*	Add a message to display in the window
	*
	*	Parameters:
	*
	*  		message - the message to display
	*/
    addMessage: function(message){
        this.messages.push(message);

        var output = "";

        this.messages.each(function(message){
            switch(typeof(message)){
                case "object":
                    message = this.objectToString(message);
            }

            output += message + "<br />";
        }.bind(this));

        this.dbgContent.update(output);

        this.dbgContent.scrollTop = this.dbgContent.scrollHeight;
    },

    /**
	*   Function: objectToString
	*
	*	Formats a JavaScript object
	*
	*	Parameters:
	*
	*  		obj - the object to format
	*		depth - the current iteration depth of the function
	*
	*	Returns:
	*
	*		A formatted string version of the object
	*/
    objectToString: function(obj, depth){
        var strOutput = "";
        var joiner = "<br />";
        var indentChar = "    ";
        var indent = "";
        var objectFound = false;
        var isArray = false;

        if(typeof(obj) == "object"){
            if(typeof(obj.length) == "number"){
                isArray = true;
            }
        }

        strOutput = (isArray == true ? "[" : "{");

        for(i = 0; i <= depth; i++){
            indent += indentChar;
        }

        $H(obj).each(function(value, key){
            switch(typeof(value[1])){
                case "object":
                    if(!objectFound){
                        depth++;
                        objectFound = true;
                    }

                    if(isArray == true){
                        strOutput += joiner + indent + this.objectToString(value[1], depth);
                    } else {
                        strOutput += joiner + indent + value[0] + ": " + this.objectToString(value[1], depth);
                    }
                    break;
                case "string":
                    if(isArray == true){
                        strOutput += joiner + indent + "\"" + value[1] + "\"";
                    } else {
                        strOutput += joiner + indent + value[0] + ": \"" + value[1] + "\"";
                    }
                    break;
                case "number":
                    if(isArray == true){
                        strOutput += joiner + indent + value[1];
                    } else {
                        strOutput += joiner + indent + value[0] + ": " + value[1];
                    }
                    break;
            }
            joiner = ",<br />";
        }.bind(this));

        strOutput += "<br />" + indent.replace(indentChar, "") + (isArray == true ? "]" : "}");

        return strOutput;
    }
});

/***********************************************************************************
************************************************************************************
***********************************************************************************/

/* JSON-P implementation for Prototype.js somewhat by Dan Dean (http://www.dandean.com)
 *
 * *HEAVILY* based on Tobie Langel's version: http://gist.github.com/145466.
 * Might as well just call this an iteration.
 *
 * This version introduces:
 * - onCreate and onFailure callback options and a full response object.
 * - option to not invoke request upon instantiation.
 *
 * Tested in Firefox 3/3.5, Safari 4
 *
 * Note: while I still think JSON-P is an inherantly flawed technique,
 * there are some valid use cases which this can provide for.
 *
 * See examples in README for usage
 */
Ajax.JSONRequest = Class.create(Ajax.Base, (function() {
    var id = 0, head = document.getElementsByTagName('head')[0];
    return {
        initialize: function($super, url, options) {
            $super(options);
            this.options.url = url;
            this.options.callbackParamName = this.options.callbackParamName || 'callback';
            this.options.timeout = this.options.timeout || 10; // Default timeout: 10 seconds
            this.options.invokeImmediately = (!Object.isUndefined(this.options.invokeImmediately)) ? this.options.invokeImmediately : true ;
            if (this.options.invokeImmediately) {
                this.request();
            }
        },

        /**
     *  Ajax.JSONRequest#_cleanup() -> "undefined"
     *  Cleans up after the request
     **/
        _cleanup: function() {
            if (this.timeout) {
                clearTimeout(this.timeout);
                this.timeout = null;
            }
            if (this.transport && Object.isElement(this.transport)) {
                this.transport.remove();
            }
        },

        /**
     *  Ajax.JSONRequest#request() -> "undefined"
     *  Invokes the JSON-P request lifecycle
     **/
        request: function() {

            // Define local vars
            var response = new Ajax.JSONResponse(this);
            var key = this.options.callbackParamName,
            name = '_prototypeJSONPCallback_' + (id++),
            complete = function() {
                if (Object.isFunction(this.options.onComplete)) {
                    this.options.onComplete.call(this, response);
                }
            }.bind(this);

            // Add callback as a parameter and build request URL
            this.options.parameters[key] = name;
            var url = this.options.url + ((this.options.url.include('?') ? '&' : '?') + Object.toQueryString(this.options.parameters));

            // Define callback function
            window[name] = function(json) {
                this._cleanup(); // Garbage collection
                window[name] = undefined;
                if (Object.isFunction(this.options.onSuccess)) {
                    response.status = 200;
                    response.statusText = "OK";
                    response.setResponseContent(json);
                    this.options.onSuccess.call(this, response);
                }
                complete();
            }.bind(this);

            this.transport = new Element('script', {
                type: 'text/javascript',
                src: url
            });

            if (Object.isFunction(this.options.onCreate)) {
                this.options.onCreate.call(this, response);
            }

            head.appendChild(this.transport);

            this.timeout = setTimeout(function() {
                this._cleanup();
                window[name] = Prototype.emptyFunction;
                if (Object.isFunction(this.options.onFailure)) {
                    response.status = 504;
                    response.statusText = "Gateway Timeout";
                    this.options.onFailure.call(this, response);
                }
                complete();
            }.bind(this), this.options.timeout * 1000);
        },
        toString: function() {
            return "[object Ajax.JSONRequest]";
        }
    };
})());

Ajax.JSONResponse = Class.create({
    initialize: function(request) {
        this.request = request;
    },
    request: undefined,
    status: 0,
    statusText: '',
    responseJSON: undefined,
    responseText: undefined,
    setResponseContent: function(json) {
        this.responseJSON = json;
        this.responseText = Object.toJSON(json);
    },
    getTransport: function() {
        if (this.request) return this.request.transport;
    },
    toString: function() {
        return "[object Ajax.JSONResponse]";
    }
});

Ajax.Responders.register({
    onCreate: function(request) {
        request['timeoutId'] = window.setTimeout(function(){
            switch (request.transport.readyState) {
                case 1:
                case 2:
                case 3:
                    request.transport.abort();
                    if (request.options['onFailure']) {
                        request.options['onFailure'](request.transport, request.json);
                    }
                    break;
            }
        }, 15000);
    },
    onComplete: function(request){
        window.clearTimeout(request['timeoutId']);
    }
});
