////////////////////////////////////////////////////////////////////////////////
//
//  Licensed to the Apache Software Foundation (ASF) under one or more
//  contributor license agreements.  See the NOTICE file distributed with
//  this work for additional information regarding copyright ownership.
//  The ASF licenses this file to You under the Apache License, Version 2.0
//  (the "License"); you may not use this file except in compliance with
//  the License.  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

package mx.messaging.channels.amfx
{

import org.apache.royale.reflection.getClassByAlias;
import mx.utils.ByteArray;
//import flash.utils.Dictionary;
import org.apache.royale.utils.net.IExternalizable;
import org.apache.royale.reflection.getDefinitionByName;

import mx.logging.Log;
import mx.messaging.errors.ChannelError;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
import mx.utils.HexDecoder;

[ResourceBundle("messaging")]

[ExcludeClass]

/**
 * Decodes an AMFX packet into an ActionScript Object graph.
 * Headers and the result body are accessed from the returned
 * AMFXResult.
 * @private
 */
public class AMFXDecoder
{
    //--------------------------------------------------------------------------
    //
    // Constructor
    // 
    //--------------------------------------------------------------------------

    public function AMFXDecoder()
    {
        super();
    }

    //--------------------------------------------------------------------------
    //
    // Class variables
    // 
    //--------------------------------------------------------------------------

	/**
	 *  @private
	 *  Storage for the resourceManager getter.
	 *  This gets initialized on first access,
	 *  not at static initialization time, in order to ensure
	 *  that the Singleton registry has already been initialized.
	 */
	private static var _resourceManager:IResourceManager;
	
	/**
	 *  @private
     *  A reference to the object which manages
     *  all of the application's localized resources.
     *  This is a singleton instance which implements
     *  the IResourceManager interface.
	 */
	private static function get resourceManager():IResourceManager
	{
		if (!_resourceManager)
			_resourceManager = ResourceManager.getInstance();

		return _resourceManager;
	}

    //--------------------------------------------------------------------------
    //
    // Public Methods
    // 
    //--------------------------------------------------------------------------
    
    public function decode(xml:XML):AMFXResult
    {
        if (xml)
        {
            var context:AMFXContext = new AMFXContext();
            context.log = Log.getLogger("mx.messaging.channels.amfx.AMFXDecoder");
            XML.ignoreWhitespace = false;
            return decodePacket(xml, context);
        }
        else
        {
            return null;
        }
    }

    //--------------------------------------------------------------------------
    //
    // Static Methods
    // 
    //--------------------------------------------------------------------------

    private static function decodePacket(xml:XML, context:AMFXContext):AMFXResult
    {
        var result:AMFXResult = new AMFXResult();

        var message:String;
		
		var name:String = xml.localName() as String;
        if (name == "amfx")
        {
            var v:String = xml.attribute("ver").toString();
            var version:uint = uint(v);

            if (supportedVersion(version))
            {
                var h:XMLList = xml.child("header");
                if (h)
                {
                    result.headers = decodeHeaders(h, context);
                }

                var body:XML = xml.body[0];

                if (body)
                {
                    result.result = decodeBody(body, context);
                }
                else
                {
					message = resourceManager.getString(
						"messaging", "noAMFXBody");
                    throw new ChannelError(message);
                }
            }
            else
            {
				message = resourceManager.getString(
					"messaging", "unsupportedAMFXVersion", [ version ]);
                throw new ChannelError(message);
            }
        }
        else
        {
			message = resourceManager.getString(
				"messaging", "noAMFXNode")
            throw new ChannelError(message);
        }

        return result;
    }

    private static function decodeHeaders(xmlList:XMLList, context:AMFXContext):Array
    {
        var headers:Array = [];

        for (var i:uint = 0; i < xmlList.length(); i++)
        {
            var h:XML = xmlList[i];
            var name:String = h.attribute("name").toString();

            var header:AMFXHeader = new AMFXHeader();
            header.name = name;

            var temp:String = h.attribute("mustUnderstand").toString();
            header.mustUnderstand = (temp == "true");

            var children:XMLList = h.children();
            if (children.length() > 0)
            {
                header.content = decodeValue(children[0], context);
            }
            else
            {
                header.content = null;
            }
            
            headers[i] = header;
        }

        return headers;
    }

    private static function decodeBody(xml:XML, context:AMFXContext):Object
    {
        var result:Object;
        var children:XMLList = xml.children();

        if (children.length() == 1)
        {
            result = decodeValue(children[0], context);
        }
        else if (children.length() > 1)
        {
            result = [];
            for (var i:uint = 0; i < children.length(); i++)
            {
                result[i] = decodeValue(children[i], context);
            }
        }

        return result;
    }

    public static function decodeValue(xml:XML, context:AMFXContext):Object
    {
        var result:Object;
        var name:String = xml.localName() as String;

        if (name == "string")
        {
            result = decodeString(xml, context);
        }
        else if (name == "true")
        {
            result = true;
        }
        else if (name == "false")
        {
            result = false;
        }
        else if (name == "array")
        {
            result = decodeArray(xml, context);
        }
        else if (name == "object")
        {
            result = decodeObject(xml, context);
        }
        else if (name == "ref")
        {
            result = decodeRef(xml, context);
        }
        /*
        else if (name == "dictionary")
        {
            result = decodeDictionary(xml, context);
        }*/
        else if (name == "double")
        {
            var n:String = xml.text().toString();
            result = Number(n);
        }
        else if (name == "int")
        {
            var i:String = xml.text().toString();
            result = int(i);
        }
        else if (name == "null")
        {
            result = null;
        }
        else if (name == "date")
        {
            result = decodeDate(xml, context);
        }
        else if (name == "xml")
        {
            var x:String = xml.text().toString();
            // If we had CDATA, restore any escaped close CDATA "]]>" tokens
            if (hasEscapedCloseCDATA(xml))
            {
                x = x.replace(REGEX_CLOSE_CDATA, "]]>");
            }

            result = new XML(x);
        }
        else if (name == "bytearray")
        {        
            result = decodeByteArray(xml);
        }
        else if (name == "undefined")
        {
            result = undefined;
        }

        return result;
    }

    private static function decodeArray(xml:XML, context:AMFXContext):Array
    {
        var array:Array = [];

        // Remember Array
        context.addObject(array);

        var entries:XMLList = xml.*;
        if (entries)
        {
            for (var i:uint = 0; i < entries.length(); i++)
            {
                var x:XML = entries[i];
                var prop:Object;
                if (x.localName() == "item")
                {
                    var propName:String = x.attribute("name").toString();
                    prop = decodeValue(x.*[0], context);
                    array[propName] = prop;
                }
                else
                {
                    prop = decodeValue(x, context);
                    array.push(prop);
                }
            }
        }

        return array;
    }

    /*
    private static function decodeDictionary(xml:XML, context:AMFXContext):Dictionary
    {
        var dictionary:Dictionary = new Dictionary();

        context.addObject(dictionary); // Remember the dictionary.

        var entries:XMLList = xml.*;
        if (entries == null)
            return dictionary;

        for (var i:uint = 0; i < entries.length(); i = i + 2)
        {
            var keyXml:XML = entries[i];
            var valueXml:XML = entries[i + 1];
            var key:Object= decodeValue(keyXml, context);
            var value:Object = decodeValue(valueXml, context);
            dictionary[key] = value;
        }

        return dictionary;
    }
    */

    private static function decodeByteArray(xml:XML):ByteArray
    {
        var str:String = xml.text().toString();

        var decoder:HexDecoder = new HexDecoder();
        decoder.decode(str);
        return decoder.drain();
    }

    private static function decodeDate(xml:XML, context:AMFXContext):Date
    {
        var d:String = xml.text().toString();
        var time:Number = new Number(d);
        var result:Date = new Date();
        result.setTime(time);

        // Remember Date object
        context.addObject(result);

        return result;
    }

    private static function decodeObject(xml:XML, context:AMFXContext):Object
    {
        var result:Object;
        var className:String = xml.attribute("type").toString();

        if (className)
        {
            try
            {
                var classType:Class = null;
                try
                {
                    classType = getClassByAlias(className);
                }
                catch(e1:Error)
                {
                    if (!classType)
                        classType = getDefinitionByName(className) as Class;
                }

                result = new classType();
            }
            catch(e:Error)
            {
                if (context.log)
                    context.log.warn("Error instantiating class: {0}. Reverting to anonymous Object.", className);

                result = {};
            }
        }
        else
        {
            className = "Object";
            result = {};
        }

        // Remember Object
        context.addObject(result);

        var entries:XMLList = xml.*;

        if (entries && entries.length() > 0)
        {
            var traits:Object;
            var tx:XML = entries[0];
			var message:String;

            if (tx.localName() == "traits")
            {
                traits = decodeTraits(tx, className, context);
                delete entries[0];
            }

            if (!traits)
            {
				message = resourceManager.getString(
					"messaging", "AMFXTraitsNotFirst")
                throw new ChannelError(message);
            }

            if (traits.externalizable)
            {
                if (result is IExternalizable)
                {
                    var ext:IExternalizable = IExternalizable(result);
                    tx = entries[0];
                    if (tx.localName() == "bytearray")
                    {
                        var ba:ByteArray = decodeByteArray(tx);

                        try
                        {
                            ext.readExternal(ba);
                        }
                        catch(e:Error)
                        {
							message = resourceManager.getString(
								"messaging", "errorReadingIExternalizable",
								[ (e.message/*+e.getStackTrace()*/) ]);
                            throw new ChannelError(message);
                        }
                    }
                }
                else
                {
					message = resourceManager.getString(
						"messaging", "notImplementingIExternalizable",
						[ className ]);
                    throw new ChannelError(message);
                }
            }
            else
            {
                for (var i:uint = 0; i < entries.length(); i++)
                {
                    var propName:String = traits.properties[i];
                    var propValue:* = decodeValue(entries[i], context);

                    try
                    {
                        result[propName] = propValue;
                    }
                    catch(e2:Error)
                    {
                        if (context.log != null)
                            context.log.warn("Cannot set property '{0}' on type '{1}'.", propName, className);
                    }
                }
            }
        }
        return result;
    }

    private static function decodeRef(xml:XML, context:AMFXContext):Object
    {
        var result:*;

		var message:String;

        var idAttr:String = xml.attribute("id").toString();
        if (idAttr)
        {
            var ref:int = parseInt(idAttr);

            if (!isNaN(ref))
            {
                result = context.getObject(ref);
            }

            if (result === undefined)
            {
				message = resourceManager.getString(
					"messaging", "unknownReference", [ idAttr ]);
                throw new ChannelError(message);
            }
        }
        else
        {
			message = resourceManager.getString(
				"messaging", "referenceMissingId");
            throw new ChannelError(message);
        }

        return result;
    }

    private static function decodeString(xml:XML, context:AMFXContext, isTrait:Boolean = false):String
    {
        var str:String;

        var refAttr:String = xml.attribute("id").toString();
        if (refAttr)
        {
            var ref:uint = uint(refAttr);
            if (!isNaN(ref))
            {
                str = context.getString(ref);
            }

            if (!str)
            {
				var message:String = resourceManager.getString(
					"messaging", "unknownStringReference", [ refAttr ]);
                throw new ChannelError(message);
            }
        }
        else
        {
            str = xml.text().toString();

            // If we had CDATA, restore any escaped close CDATA "]]>" tokens
            // Note that trait names won't have CDATA sections... so no need to check them
            if (!isTrait && hasEscapedCloseCDATA(xml))
            {
                str = str.replace(REGEX_CLOSE_CDATA, "]]>");
            }

            //Remember string
            context.addString(str);
        }

        return str;
    }

    private static function decodeTraits(xml:XML, className:String, context:AMFXContext):Object
    {
        var traits:Object;

        var refAttr:String = xml.attribute("id").toString();
        if (refAttr)
        {
            var ref:uint = uint(refAttr);
            if (!isNaN(ref))
            {
                traits = context.getTraitInfo(ref);
            }

            if (!traits)
            {
				var message:String = resourceManager.getString(
					"messaging", "unknownTraitReference", [ refAttr ]);
                throw new ChannelError(message);
            }
        }
        else
        {
            traits = {};
            traits.name = className;
            traits.alias = className;
            traits.properties = [];
            traits.externalizable = false;

            var ext:String = xml.attribute("externalizable").toString();
            if (ext == "true")
            {
                traits.externalizable = true;
            }

            var nodes:XMLList = xml.*;
            if (nodes)
            {
                for (var i:uint = 0; i < nodes.length(); i++)
                {
                    traits.properties[i] = decodeString(nodes[i], context, true);
                }
            }

            //Remember traits
            context.addTraitInfo(traits);
        }

        return traits;
    }

    private static function hasEscapedCloseCDATA(xml:XML):Boolean
    {
        var s:String = xml.toXMLString();

        if (s.indexOf("]]>") != -1)
        {
            return s.indexOf("]]&gt;") != -1;
        }
        else
        {
            return false;
        }
    }

    private static function supportedVersion(ver:uint):Boolean
    {
        for (var i:uint = 0; i < SUPPORTED_VERSIONS.length; i++)
        {
            if (ver == SUPPORTED_VERSIONS[i])
                return true;
        }

        return false;
    }

    //--------------------------------------------------------------------------
    //
    // Static Constants
    // 
    //--------------------------------------------------------------------------

    private static const SUPPORTED_VERSIONS:Array = [3];
    private static const REGEX_CLOSE_CDATA:RegExp = new RegExp("]]&gt;", "g");
}

}
