/*
 Copyright (c) 2006 - 2008  Eric J. Feminella  <eric@ericfeminella.com>
 All rights reserved.

 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.

 @internal
 */
 
package com.ericfeminella.application
{
    import com.ericfeminella.collections.HashMap;
    import com.ericfeminella.collections.IMap;
    import flash.external.ExternalInterface;
    import mx.core.Application;
    
    /**
     * 
     * Provides an API which allows for detailed inspection and
     * modification of an application QueryString.
     * 
     * <p>
     * QueryString supports parsing of a QueryString which has been
     * provided to the application via a user agent. Additionally,
     * support for specific QueryStrings is provided which allows
     * for detailed inspection of arbitrary query strings via the
     * constructor
     * </p>
     * 
     * @see flash.external.ExternalInterface
     * @see mx.core.Application
     * 
     */    
    public class QueryString implements IQueryString
    {
        /**
         * 
         * Uniform Resource Identifier (URI) to which the querystring 
         * has been provided
         * 
         * @example
         * <listing version="3.0">
         * 
         * var url:String = "http://localhost/test.html?version=3";
         * // uri: http://localhost/test.html
         * 
         * </listing>
         * 
         */
        protected var uri:String;
        
        /**
         * 
         * The querystring which has been provided via the application
         * URL or specified in the constructor, any modification to the
         * querystring are reflected in this String
         * 
         * @example
         * <listing version="3.0">
         * 
         * var url:String = "http://localhost/test.html?version=3";
         * // uri: version=3
         * 
         * </listing> 
         * 
         */
        protected var querystring:String;
        
        /**
         * 
         * Defines the name / value pairs which have been supplied 
         * to the querystring, any modification to the querystring 
         * are reflected in this HashMap
         * 
         */
        protected var parameters:IMap;
        
        /**
         *
         * By default the QueryString constructor will use the applications
         * querystring and decode it unless specified otherwise 
         * 
         * <p>
         * Additionally, support for specific QueryStrings is provided which 
         * allows for detailed inspection of arbitrary query strings via the
         * constructor
         * </p>
         * 
         * <p>
         * The QueryString constructor takes a single url as an argument. 
         * This argument is optional, and, if specified instructs the 
         * QueryString object to use the specified url for all subsequent 
         * operations. If the url is not specified the QueryString object 
         * will assume the aplication query string is to be used for all 
         * operations.
         * </p>
         * 
         * <p>
         * Unfortunately, ActionScript 3 does not support constructor / 
         * method overloading so the url parameter is used to achive the 
         * correct functionality based on context.
         * </p>
         * 
         * @example
         * <listing version="3.0">
         * 
         * var querystring:IQueryString = new QueryString();
         * //defaults to application querystring
         * 
         * trace ( querystring.getQueryString() );
         * //outputs application querystring
         * 
         * </listing>
         * 
         * <p>
         * Optionally, you can opt to use a specific QueryString by passing 
         * it to the constructor:
         * </p>
         * 
         * <listing version="3.0">
         * 
         * var url:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3";
         * 
         * var querystring:IQueryString = new QueryString();
         * //defaults to: name=Adobe Flex&version=3
         * 
         * trace ( querystring.getQueryString() );
         * //name=Adobe Flex&version=3
         * 
         * </listing>
         * 
         * 
         * @param an optional url from which the query string is to be based
         * @param specifies if the query string is to be URL decoded
         * @see #build
         * 
         */        
        public function QueryString(url:String = null, decode:Boolean = true)
        {
            this.build( url, decode );
            this.createParameters();
        }
        
        /**
         * 
         * Determines if parameters have been provided to the application
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri );
         * trace( querystring.isProvided );
         * //true
         * 
         * </listing>
         * 
         * @return true if supplied, false if not
         * 
         */
        public function get isProvided() : Boolean
        {
            return parameters.size() > 0;
        }
        
        /**
         * 
         * Retrieves the raw query string provided to the application
         * 
         * @example
         * <listing version="3.0>
         * 
         * var url:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( url );
         * 
         * trace( querystring.getQueryString() );
         * //name=Adobe%20Flex&version=3.0
         * 
         * //QueryString Object with decode set to true
         * 
         * trace( querystring.getQueryString(true) );
         * //name=Adobe Flex&version=3.0
         * 
         * </listing>
         * 
         * @param  specifies if the querystring is to be URL decoded /encoded
         * @return the raw query string which has been supplied
         * 
         */   
        public function getQueryString(decode:Boolean = true) : String
        {
            return decode ? decodeURIComponent( querystring ) : encodeURIComponent( querystring );
        }
        
        /**
         * 
         * Retrieves all name / value pair provided to the application and 
         * returns an Array of Objects which contain each parameter name 
         * and value.
         * 
         * <p>
         * <code>QueryString.getParameters()</code> returns a new HashMap
         * which contains all key / value pairs. A reference to the internal
         * HashMap instance is preserved, the <code>IMap</code> returned is
         * a copy of the key / values from the internal <code>HashMap</code>
         * </p>
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri );
         * 
         * var map:IMap = querystring.getParameters() );
         * trace( map.getValues() );
         * // Adobe Flex,3.0
         * 
         * </listing>
         * 
         * @return IMap containing key / values as specified in the querystring
         * 
         */  
        public function getParameters() : IMap
        {
            var map:IMap = new HashMap();
            
            var keys:Array = parameters.getKeys();
            var n:int = keys.length;
            
            for (var i:int = 0; i < n; i++)
            {
                var key:* = keys[i];
                var value:* = parameters.getValue( keys[i] );
                map.put( key, value );
            }
            return map;
        }
        
        /**
         * 
         * Adds a new parameter ( name / value ) pair to the querystring
         * 
         * <p>
         * If the parameter name which has been specified currently exists
         * in the querystring then it is simply ignored. 
         * </p>
         * 
         * <p>
         * If you need to overwrite an existing parameter or modify the value 
         * of an existing parameter use <code>setValue</code> instead
         * </p>
         * 
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri );
         * 
         * trace( querystring.getQueryString(true) );
         * //name=Adobe Flex&version=3.0
         * 
         * //add an additional parameter to the querystring
         * querystring.addParameter("year", 2008);
         * 
         * trace( querystring.getQueryString( true ) );
         * //name=Adobe Flex&version=3.0&year=2008
         * 
         * </listing>
         * 
         * 
         * @param  the name of the parameter which is to be added to the QueryString
         * @param  the value to assigned to the specified parameter name
         * 
         */       
        public function addParameter(name:String, value:*) : void
        {
            if ( !parameters.containsKey(name) )
            {
                parameters.put( name, value.toString() );
                update();
            }
        }
        
        /**
         * 
         * Removes an existing parameter from the querystring
         * 
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri );
         * 
         * trace( querystring.getQueryString(true) );
         * //name=Adobe Flex&version=3.0
         * 
         * //remove a parameter from the querystring
         * querystring.removeParameter("version");
         * 
         * trace( querystring.getQueryString( true ) );
         * //name=Adobe Flex
         * 
         * </listing>
         * 
         * @param  the name of the parameter which is to be removed
         * 
         */
        public function removeParameter(name:String) : void
        {
            parameters.remove( name );
            update();
        }
        
        /**
         * 
         * Determines if the specified parameter has been provided to 
         * the query string
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri );
         * trace( querystring.containsParameter("name") );
         * //true
         * 
         * </listing>
         * 
         * @param  the paramter to determine
         * @return true if supplied, false if not
         * 
         */  
        public function containsParameter(name:String) : Boolean
        {
            return parameters.containsKey( name );
        }
                  
        /**
         * 
         * Retrieves the parameter names provided to the application
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri );
         * trace( querystring.getNames() );
         * //name, version
         * 
         * </listing>
         * 
         * @return each query string parameter name
         * 
         */  
        public function getNames() : Array
        {
            return parameters.getKeys();
        }
               
        /**
         * 
         * Retrieves a specific parameter name based on the parameters
         * value
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri );
         * trace( querystring.getName(3.0) );
         * //version
         * 
         * </listing>
         * 
         * @param  the value of the parameter which is to be located
         * @return parameter name to which the specified value has been assigned
         * 
         */  
        public function getName(value:String) : String
        {
            return parameters.getKey( value );
        }
        
        /**
         * 
         * Retrieves the parameter values provided to the application
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri, true );
         * trace( querystring.getValues() );
         * //Adobe Flex, 3.0
         * 
         * </listing>
         * 
         * @return each query string parameter value
         * 
         */ 
        public function getValues() : Array
        {
            return parameters.getValues();
        }

        /**
         * 
         * Retrieves the value of the specified parameter name. If the 
         * parameter does not exist a null value is returned. If the 
         * name has not been provided in the querystring then a null
         * value is returned
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri, true );
         * trace( querystring.getValue("version") );
         * //3.0
         * 
         * </listing>
         * 
         * @param  the name of the parameter
         * @return the value of the specified paramter name
         * 
         */
        public function getValue(name:String) : *
        {
            trace( parameters.getValue( name ) );
            if ( parameters.getValue( name ) == "false" || parameters.getValue( name ) == "true")
            {
                return parameters.getValue( name ) == "false" ? false : true;
            }
            return parameters.getValue( name );
        }
        
        /**
         * 
         * Assigns a new value to the specified querystring parameter
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri, true );
         * 
         * trace( querystring.getValue("version") );
         * //3.0
         * 
         * querystring.setValue("version", 2.1)
         * 
         * trace( querystring.getValue("version") );
         * //2.1
         * 
         * </listing>
         *  
         * @param  the name of the parameter
         * @param  the value to assigned to the specified parameter name
         * 
         */        
        public function setValue(name:String, value:*) : void
        {
            parameters.put( name,  value.toString() );
            update();
        }
        
        /**
         * 
         * Determines if the specified value has been provided to the 
         * query string
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri );
         * trace( querystring.containsValue(3.0) );
         * //true
         * 
         * </listing>
         * 
         * @param  the value in which to determine 
         * @return true if the value has been provided, false if not
         * 
         */
        public function containsValue(value:*) : Boolean
        {
            return parameters.containsValue( value ); 
        }
        
        /**
         * 
         * Appends the current state of the <code>querystring</code>
         * to a URL
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri, true );
         * 
         * var url:String = "http://127.0.0.1/apps/test.html";
         * url = querystring.appendToURL( url );
         * 
         * trace( url );
         * //http://127.0.0.1/apps/test.html?name=Adobe Flex&version=3.0;
         * 
         * </listing>
         * 
         * @param  the url to which the querystring is to be appended
         * @param  specifies if the querystring is to be URL decoded /encoded
         * @return the specified url with the querystring appended
         * 
         */        
        public function appendToURL(url:String, decode:Boolean = true) : String
        {
            if ( url.indexOf("?") != -1 )
            {
                url += "&" + this.querystring;
            }
            else
            {
                url += "?" + this.querystring;
            }
            
            return decode ? decodeURI( url ) : encodeURI( url );;
        }
        
        /**
         * 
         * Determines the length of the query string based on the amount
         * of name value pairs which have been supplied to the QueryString
         * 
         * @example
         * <listing version="3.0>
         * 
         * var uri:String = "http://127.0.0.1/test.html?name=Adobe Flex&version=3.0";
         * 
         * var querystring:IQueryString = new QueryString( uri );
         * trace( querystring.length() );
         * // 2
         * 
         * </listing>
         * 
         * @return the length of the query string
         * 
         */
        public function length() : int
        {
            return parameters.size();
        }
        
        /**
         * 
         * Builds the QueryString Object and members from the specified
         * URL or Application URL
         * 
         * @param an optional url from which the query string is to be based
         * @param specifies if the query string is to be URL decoded
         * @see #createParameters
         * 
         */        
        protected function build(url:String = null, decode:Boolean = true) : void
        {
            var parts:Array;

            if (url == null)
            {
                if ( ExternalInterface.available )
                {
                    this.uri = ExternalInterface.call( "window.location.href.toString" );

                    parts = this.uri.split("?");
                    querystring = decode == true ? decodeURIComponent( parts[1] ) : parts[1];
                }
            }
            else
            {
                parts = url.split("?");
                uri = parts[0];
                querystring = decode ? decodeURIComponent( parts[1] ) : parts[1];
            }
        }
        
        /**
         * 
         * Parses the <code>querystring</code> into an Array of Objects.
         * 
         * <p>
         * Each containing a specific name property and a value property 
         * which contains the values of each name / value pair in provided
         * in the querystring
         * </p>
         * 
         */        
        protected function createParameters() : void
        {
            parameters  = new HashMap( true );

            var nameValuePairs:Array = querystring.split("&");
            var n:int = nameValuePairs.length;
            
            for (var i:int = 0; i < n; i++) 
            {
                var parts:Array  = nameValuePairs[i].split("=");
                var name:String  = parts[0];
                var value:String = parts[1];

                parameters.put(name, value);
            }            
        }
        
        /**
         * 
         * Updates the querystring to reflect changes made via 
         * <code>setValue()</code>, <code>addParameter()</code>
         * and <code>removeParameter()</code>
         * 
         * @see #setValue
         * @see #addParameter
         * @see #removeParameter
         * 
         */        
        protected final function update() : void
        {
            var qs:String = "";
            
            for (var name:String in parameters)
            {
                qs += name + "=" + parameters[name] + "&";
            }
            
            this.querystring = qs.slice( 0, qs.length -1 );
        }
    }
}