/*
 Copyright (c) 2006  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.collections
{
    import flash.net.ObjectEncoding;
    import flash.net.SharedObject;
    import flash.utils.Dictionary;

    /**
     *
     * Provides an <code>IMap</code> implementation into the <code>data</code>
     * object of a <code>SharedObject</code> on a clients local file system.
     *
     * <p>
     * <code>LocalPersistenceMap</code> allows the <code>data</code> object
     * of a <code>SharedObject</code> to be accessed via an <code>IMap</code>
     * implementation in order to provide a consistant API for working with
     * the underlying data of the <code>SharedObject</code>.
     * </p>
     *
     * @example The following example demonstrates how a client implementation
     * of <code>LocalPersistenceMap</code> can be utilized to provide a typical
     * <code>IMap</code> implementation into a <code>SharedObject</code>.
     *
     * <listing version="3.0">
     *
     * var map:IMap = new LocalPersistenceMap("test", "/");
     * map.put("username", "efeminella");
     * map.put("password", "43kj5k4nr43r934hcr34hr8h3");
     * map.put("admin", true);
     *
     * </listing>
     *
     * @see com.ericfeminella.collections.IMap
     * @see http://livedocs.adobe.com/flex/3/langref/flash/net/SharedObject.html
     *
     */
    public class LocalPersistenceMap implements IMap
    {
        /**
         *
         * Defines the underlying <code>SharedObject</code> instance in which
         * the persistant <code>data</code> is stored.
         *
         */
        protected var sharedObject:SharedObject = null;

        /**
         *
         * Defines a local reference to the <code>data</code> property of the
         * underlying <code>SharedObject</code> instance.
         *
         * @see http://livedocs.adobe.com/flex/3/langref/flash/net/SharedObject.html#data
         *
         */
        protected var data:Object = null;

        /**
         *
         * Defines the minimum disc space which is required by the persistant
         * <code>SharedObject</code>.
         *
         */
        protected var minimumStorage:Number = NaN;

        /**
         *
         * <ocde>LocalPersistenceMap</code> constructor creates a reference to
         * the persisted <code>SharedObject</code> available from the clients
         * local disk. If the <code>SharedObject</code> does not currently exist,
         * Flash Player will attempt to created it.
         *
         * <p>
         * If the identifier parameter contains any invalid charachters, they will
         * be substituted with underscores
         * </p>
         *
         * @param  name of the local <code>SharedObject</code>
         * @param  the local path to the <code>SharedObject</code>
         * @param  specifies if the shared object is from a secure domain
         * @param  minimum amount of disc space required by the shared object
         *
         * @return true if the write operation was successful, false if not
         *
         */
        public function LocalPersistenceMap(identifier:String, localPath:String = null, secure:Boolean = false, minimumStorage:int = 500)
        {
            var pattern:RegExp = /[~%&\;:\"\'<>\?#]/g;
            identifier = identifier.replace( pattern, "_" );

            sharedObject = SharedObject.getLocal( identifier, localPath, secure );
            sharedObject.objectEncoding = ObjectEncoding.AMF3;
            sharedObject.flush( minimumStorage );

            this.data = sharedObject.data;
            this.minimumStorage = minimumStorage;
        }

        /**
         *
         * Adds a key and value to the <code>data</code> object of the
         * underlying the <code>SharedObject</code> instance.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "user", userVO );
         *
         * </listing>
         *
         * @param the key to add to the map
         * @param the value of the specified key
         *
         */
        public function put(key:*, value:*) : void
        {
            sharedObject.data[key] = value;
            sharedObject.flush( minimumStorage );
        }

        /**
         *
         * Places all name / value pairs into the current
         * <code>IMap</code> instance.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var table:Object = {a: "foo", b: "bar"};
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.putAll( table );
         *
         * trace( map.getKeys() ); // a, b
         * trace( map.getValues() ); // foo, bar
         *
         * </listing>
         *
         * @param an <code>Object</code> of name / value pairs
         *
         */
        public function putAll(table:Dictionary) : void
        {
            for (var prop:String in table)
            {
                put( prop, table[prop] );
            }
        }

        /**
         *
         * <code>putEntry</code> is intended as a pseudo-overloaded
         * <code>put</code> implementation whereby clients may call
         * <code>putEntry</code> to pass an <code>IHashMapEntry</code>
         * implementation.
         *
         * @param concrete <code>IHashMapEntry</code> implementation
         *
         */
        public function putEntry(entry:IHashMapEntry) : void
        {
            put( entry.key, entry.value );
        }

        /**
         *
         * Removes a key and value from the <code>data</code> object of the
         * underlying the <code>SharedObject</code> instance.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         * map.remove( "admin" );
         *
         * </listing>
         *
         * @param the key to remove from the map
         *
         */
        public function remove(key:*) : void
        {
            delete sharedObject.data[key];
            sharedObject.flush( minimumStorage );
        }

        /**
         *
         * Determines if a key exists in the <code>data</code> object of the
         * underlying the <code>SharedObject</code> instance.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         *
         * trace( map.containsKey( "admin" ) ); //true
         *
         * </listing>
         *
         * @param  the key in which to determine existance in the map
         * @return true if the key exisits, false if not
         *
         */
        public function containsKey(key:*) : Boolean
        {
            return sharedObject.data.hasOwnProperty( key );
        }

        /**
         *
         * Determines if a value exists in the HashMap instance
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         *
         * trace( map.containsValue( adminVO ) ); //true
         *
         * </listing>
         *
         * @param  the value in which to determine existance in the map
         * @return true if the value exisits, false if not
         *
         */
        public function containsValue(value:*) : Boolean
        {
            var result:Boolean = false;

            for ( var prop:String in data )
            {
                if ( data[prop] == value )
                {
                    result = true;
                    break;
                }
            }
            return result;
        }

        /**
         *
         * Returns a key value from the HashMap instance
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         *
         * trace( map.getKey( adminVO ) ); //admin
         *
         * </listing>
         *
         * @param  the key in which to retrieve the value of
         * @return the value of the specified key
         *
         */
        public function getKey(value:*) : *
        {
            var key:String = "";

            for ( var prop:String in data )
            {
                if ( data[prop] == value )
                {
                    key = prop;
                    break;
                }
            }
            return key;
        }

        /**
         *
         * Returns each key added to the <code>data</code> object of the
         * underlying the <code>SharedObject</code> instance.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         * map.put( "editor", editorVO );
         *
         * trace( map.getKeys() ); //admin, editor
         *
         * </listing>
         *
         * @return Array of key identifiers
         *
         */
        public function getValue(key:*) : *
        {
            var value:*;

            for ( var prop:String in data )
            {
                if ( prop == key )
                {
                    value = data[prop];
                    break;
                }
            }
            return value;
        }

        /**
         *
         * Returns each key added to the <code>data</code> object of the
         * underlying the <code>SharedObject</code> instance.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         * map.put( "editor", editorVO );
         *
         * trace( map.getKeys() ); //admin, editor
         *
         * </listing>
         *
         * @return Array of key identifiers
         *
         */
        public function getKeys() : Array
        {
            var keys:Array = new Array();

            for ( var prop:String in data )
            {
                keys.push( prop );
            }
            return keys;
        }

        /**
         *
         * Retrieves each value assigned to the <code>data</code> object of
         * the underlying the <code>SharedObject</code> instance.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         * map.put( "editor", editorVO );
         *
         * trace( map.getValues() ); //[object, adminVO],[object, editorVO]
         *
         * </listing>
         *
         * @return Array of values assigned for all keys in the map
         *
         */
        public function getValues() : Array
        {
            var values:Array = new Array();

            for ( var prop:String in data )
            {
                values.push( data[prop] );
            }
            return values;
        }

        /**
         *
         * Determines the size of the <code>data</code> object of the
         * underlying the <code>SharedObject</code> instance.e
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         * map.put( "editor", editorVO );
         *
         * trace( map.size() ); //2
         *
         * </listing>
         *
         * @return the current size of the map instance
         *
         */
        public function size() : int
        {
            var length:int = 0;

            for ( var key:* in data )
            {
                length++;
            }
            return length;
        }

        /**
         *
         * Determines if the current <code>data</code> object of the
         * underlying the <code>SharedObject</code> instance is empty.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * trace( map.isEmpty() ); //true
         *
         * map.put( "admin", adminVO );
         * trace( map.isEmpty() ); //false
         *
         * </listing>
         *
         * @return true if the current map is empty, false if not
         *
         */
        public function isEmpty() : Boolean
        {
            return size() <= 0;
        }

        /**
         *
         * Resets all key / value assignment in the <code>data</code> object
         * of the underlying the <code>SharedObject</code> instance is empty
         * to null.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         * map.put( "editor", editorVO );
         * map.reset();
         *
         * trace( map.getValues() ); //null, null
         *
         * </listing>
         *
         */
        public function reset() : void
        {
            for ( var prop:String in data )
            {
                data[prop] = undefined;
            }
            sharedObject.flush( minimumStorage );
        }

        /**
         *
         * Resets all key / values defined in the <code>data</code> object of
         * the underlying the <code>SharedObject</code> instance is empty to
         * null with the exception of the specified key.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         * map.put( "editor", editorVO );
         *
         * trace( map.getValues() ); //[object, adminVO],[object, editorVO]
         *
         * map.resetAllExcept( "editor", editorVO );
         * trace( map.getValues() ); //null, [object, editorVO]
         *
         * </listing>
         *
         * @param the key which is not to be cleared from the map
         *
         */
        public function resetAllExcept(key:*) : void
        {
            for ( var prop:String in data )
            {
                if ( prop != key )
                {
                    data[prop] = undefined;
                }
            }
            sharedObject.flush( minimumStorage );
        }

        /**
         *
         * Resets all key / values in the <code>data</code> object of the
         * underlying the <code>SharedObject</code> instance  to null.
         *
         * @example
         *
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         * map.put( "editor", editorVO );
         *
         * trace( map.size() ); //2
         *
         * map.clear();
         *
         * trace( map.size() ); //0
         *
         * </listing>
         *
         */
        public function clear() : void
        {
            for ( var key:* in data )
            {
                remove( key );
            }
            sharedObject.flush( minimumStorage );
        }

        /**
         *
         * Clears all key / values defined in the <code>data</code> object
         * of the underlying the <code>SharedObject</code> instance with the
         * exception of the specified key.
         *
         * @example
         *
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:IMap = new LocalPersistenceMap("sharedObjectName");
         * map.put( "admin", adminVO );
         * map.put( "editor", editorVO );
         * trace( map.size() ); //2
         *
         * map.clearAllExcept( "editor", editorVO );
         * trace( map.getValues() ); //[object, editorVO]
         * trace( map.size() ); //1
         *
         * </listing>
         *
         * @param the key which is not to be cleared from the map
         *
         */
        public function clearAllExcept(key:*) : void
        {
            for ( var prop:String in data )
            {
                if ( prop != key )
                {
                    delete data[prop];
                }
            }
            sharedObject.flush( minimumStorage );
        }

        /**
         *
         * Returns an <code>IList</code> of <code>IHashMapEntry</code>
         * objects based on the underlying internal map.
         *
         * @param <code>IList</code> of <code>IHashMapEntry</code> objects
         *
         */
        public function getEntries() : Array
        {
            var list:Array = new Array();

            for ( var key:String in data )
            {
                list.push( new HashMapEntry( key, data[key] ) );
            }
            return list
        }

        /**
         *
         * Retrieves the underlying <code>SharedObject</code> instance
         * used by the <code>LocalPersistenceMap</code>.
         *
         * @example
         * <listing version="3.0">
         *
         * import com.ericfeminella.collections.LocalPersistenceMap;
         * import com.ericfeminella.collections.IMap;
         *
         * var map:LocalPersistenceMap = new LocalPersistenceMap("sharedObjectName");
         * trace( map.sharedObjectInstance );
         *
         * </listing>
         *
         * @return underlying <code>SharedObject</code> instance
         *
         */
        public function get sharedObjectInstance() : SharedObject
        {
            return sharedObject;
        }
    }
}