////////////////////////////////////////////////////////////////////////////////
//
//  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
{

import org.apache.royale.events.EventDispatcher;

import mx.core.IMXMLObject;
import mx.core.mx_internal;
import mx.events.PropertyChangeEvent;
import mx.logging.ILogger;
import mx.logging.Log;
import mx.messaging.channels.PollingChannel;
import mx.messaging.config.ConfigMap;
import mx.messaging.config.ServerConfig;
import mx.messaging.errors.InvalidDestinationError;
import mx.messaging.events.ChannelEvent;
import mx.messaging.events.ChannelFaultEvent;
import mx.messaging.events.MessageAckEvent;
import mx.messaging.events.MessageEvent;
import mx.messaging.events.MessageFaultEvent;
import mx.messaging.messages.AbstractMessage;
import mx.messaging.messages.AcknowledgeMessage;
import mx.messaging.messages.CommandMessage;
import mx.messaging.messages.ErrorMessage;
import mx.messaging.messages.IMessage;
import mx.messaging.messages.MessagePerformanceUtils;
import mx.netmon.NetworkMonitor;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
import mx.utils.Base64Encoder;
import mx.utils.UIDUtil;

use namespace mx_internal;

/**
 *  Dispatched when an acknowledge message is received for a sent message.
 *
 *  @eventType mx.messaging.events.MessageAckEvent.ACKNOWLEDGE
 *
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion BlazeDS 4
 *  @productversion LCDS 3    
 *
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion BlazeDS 4
 *  @productversion LCDS 3       
 */
[Event(name="acknowledge", type="mx.messaging.events.MessageAckEvent")]

/**
 *  Dispatched when a message fault occurs.
 *
 *  @eventType mx.messaging.events.MessageFaultEvent.FAULT
 *
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion BlazeDS 4
 *  @productversion LCDS 3 
 */
[Event(name="fault", type="mx.messaging.events.MessageFaultEvent")]

/**
 *  Dispatched when the underlying Channel the MessageAgent is using connects.
 *
 *  @eventType mx.messaging.events.ChannelEvent.CONNECT
 *
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion BlazeDS 4
 *  @productversion LCDS 3 
 */
[Event(name="channelConnect", type="mx.messaging.events.ChannelEvent")]

/**
 *  Dispatched when the underlying Channel the MessageAgent is using disconnects.
 *
 *  @eventType mx.messaging.events.ChannelEvent.DISCONNECT
 *
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion BlazeDS 4
 *  @productversion LCDS 3 
 */
[Event(name="channelDisconnect", type="mx.messaging.events.ChannelEvent")]

/**
 *  Dispatched when the underlying Channel the MessageAgent is using faults.
 *
 *  @eventType mx.messaging.events.ChannelFaultEvent.FAULT
 *
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion BlazeDS 4
 *  @productversion LCDS 3 
 */
[Event(name="channelFault", type="mx.messaging.events.ChannelFaultEvent")]

/**
 *  Dispatched when the <code>connected</code> property of the MessageAgent changes.
 *  Also dispatched when the <code>subscribed</code> of a Consumer changes.
 *  @see mx.messaging.Consumer
 *  @eventType mx.events.PropertyChangeEvent.PROPERTY_CHANGE
 *
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion BlazeDS 4
 *  @productversion LCDS 3 
 */
[Event(name="propertyChange", type="mx.events.PropertyChangeEvent")]

//[ResourceBundle("messaging")]

/**
 *  The MessageAgent class provides the basic low-level functionality common to
 *  message handling for a destination.
 *
 *  <p><b>Note:</b> For advanced use only.
 *  Use this class for creating custom message agents like the existing Producer
 *  and Consumer classes.</p>
 *
 *  @mxml
 *  <p>
 *  All message agent classes, including the Producer and Consumer classes, extend
 *  MessageAgent and inherit the following tag attributes:
 *  </p>
 *  <pre>
 *   &lt;mx:<i>tagname</i><br>
 *    <b>Properties</b>
 *    channelSet="<i>No default.</i>"
 *    clientId="<i>No default.</i>"
 *    connected="<i>false</i>"
 *    destination="<i>No default.</i>"
 *    requestTimeout="<i>-1</i>"
 *    subtopic="<i>No default.</i>"
 *
 *
 *   <b>Events</b>
 *    acknowledge="<i>No default.</i>"
 *    channelConnect="<i>No default.</i>"
 *    channelDisconnect="<i>No default.</i>"
 *    channelFault="<i>No default.</i>"
 *    fault="<i>No default.</i>"
 *    propertyChange="<i>No default.</i>"
 *  /&gt;
 *  </pre>
 *
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion BlazeDS 4
 *  @productversion LCDS 3 
 */
public class MessageAgent extends EventDispatcher implements IMXMLObject
{
    //--------------------------------------------------------------------------
    //
    // Internal Static Constants
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Indicates that the MessageAgent is used an automatically configured ChannelSet
     *  obtained from ServerConfig.
     */
    mx_internal static const AUTO_CONFIGURED_CHANNELSET:int = 0;

    /**
     *  @private
     *  Indicates that the MessageAgent is using a manually assigned ChannelSet.
     */
    mx_internal static const MANUALLY_ASSIGNED_CHANNELSET:int = 1;

    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3           
     */
    public function MessageAgent()
    {
        super();
    }

    //--------------------------------------------------------------------------
    //
    // Variables
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  The type of MessageAgent.
     *  This variable is used for logging and MUST be assigned by subclasses.
     */
    protected var _agentType:String = "mx.messaging.MessageAgent";

    /**
     *  @private
     *  The Base64 encoded credentials that will be passed through to
     *  the ChannelSet.
     */
    protected var _credentials:String;

    /**
     *  @private
     *  The character set encoding used to create the credentials String.
     */
    protected var _credentialsCharset:String;

    /**
     *  @private
     *  Indicates whether the agent is explicitly disconnected.
     *  This allows agents to supress processing of acks/faults that return
     *  after the client has issued an explicit disconnect().
     */
    protected var _disconnectBarrier:Boolean;

    /**
     *  @private
     *  This helps in the runtime configuration setup by delaying the connect
     *  event until the configuration has been setup. See acknowledge().
     */
    private var _pendingConnectEvent:ChannelEvent;

    /**
     *  @private
     *  The Base64 encoded credentials that are passed through to a
     *  3rd party.
     */
    private var _remoteCredentials:String = "";

    /**
     *  @private
     *  The character set encoding used to create the remoteCredentials String.
     */
    private var _remoteCredentialsCharset:String;

    /**
     *  @private
     *  Indicates that the remoteCredentials value has changed and should
     *  be sent to the server.
     */
    private var _sendRemoteCredentials:Boolean;

    /**
     *  @private
     *  The logger MUST be assigned by subclasses, for example
     *  Consumer and Producer.
     */
    protected var _log:ILogger;

    /**
     *  @private
     *  A queue to store pending outbound messages while waiting for a server response
     *  that contains a server-generated clientId.
     *  Serializing messages from a MessageAgent to the server is essential until we
     *  receive a response containing a server-generated clientId; otherwise the server
     *  will treat each message as if it was sent by a different, "new" MessageAgent instance.
     */
    private var _clientIdWaitQueue:Array;

    /**
     *  @private
     * Flag being set to true denotes that we should skip remaining fault
     * processing logic because the fault has already been handled.
     * Currently used during an automatic resend of a faulted message if the fault
     * was due to a server session timeout and is authentication/authorization related.
     */
    protected var _ignoreFault:Boolean = false;

    /**
     *  @private
     */
    private var resourceManager:IResourceManager =
                                    ResourceManager.getInstance();
    //--------------------------------------------------------------------------
    //
    // Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  authenticated
    //----------------------------------

    /**
     *  @private
     */
    private var _authenticated:Boolean;

    [Bindable(event="propertyChange")]
    /**
     *  Indicates if this MessageAgent is using an authenticated connection to
     *  its destination.
     */
    public function get authenticated():Boolean
    {
        return _authenticated;
    }

    /**
     *  @private
     */
    mx_internal function setAuthenticated(value:Boolean, creds:String):void
    {
        if (_authenticated != value)
        {
            var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "authenticated", _authenticated, value);
            _authenticated = value;
            dispatchEvent(event);

            if (value)
                assertCredentials(creds);
        }
    }

    //----------------------------------
    //  channelSet
    //----------------------------------

    /**
     *  @private
     */
    private var _channelSet:ChannelSet;

    [Bindable(event="propertyChange")]
    /**
     *  Provides access to the ChannelSet used by the MessageAgent. The
     *  ChannelSet can be manually constructed and assigned, or it will be
     *  dynamically initialized to use the configured Channels for the
     *  destination for this MessageAgent.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3           
     */
    public function get channelSet():ChannelSet
    {
        return _channelSet;
    }

    /**
     *  @private
     */
    public function set channelSet(value:ChannelSet):void
    {
        internalSetChannelSet(value);
        _channelSetMode = MANUALLY_ASSIGNED_CHANNELSET;
    }

    /**
     *  @private
     *  This method is called by ChannelSet.connect(agent) to set up the bidirectional
     *  relationship between the MessageAgent and the ChannelSet.
     *  It also handles the case of customer code calling channelSet.connect(agent)
     *  directly rather than assigning the ChannelSet to the MessageAgent's channelSet
     *  property.
     */
    mx_internal function internalSetChannelSet(value:ChannelSet):void
    {
        if (_channelSet != value)
        {
            if (_channelSet != null)
                _channelSet.disconnect(this);

            var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "channelSet", _channelSet, value);
            _channelSet = value;

            if (_channelSet != null)
            {
                if (_credentials)
                    _channelSet.setCredentials(_credentials, this, _credentialsCharset);

                _channelSet.connect(this);
            }

            dispatchEvent(event);
        }
    }

    //----------------------------------
    //  clientId
    //----------------------------------

    /**
     *  @private
     */
    private var _clientId:String;

    [Bindable(event="propertyChange")]
    /**
     *  Provides access to the client id for the MessageAgent.
     *  MessageAgents are assigned their client id by the remote destination
     *  and this value is used to route messages from the remote destination to
     *  the proper MessageAgent.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3           
     */
    public function get clientId():String
    {
        return _clientId;
    }

    /**
     *  @private
     *  This method is used to assign a server-generated client id to the MessageAgent
     *  in the common scenario.
     *  It may also be used by the framework to sync up cooperating MessageAgents under
     *  a single client id value so that they appear as a single MessageAgent to the server.
     *  Assigning a client id value will flush any messages that have been queued while we
     *  were waiting for a server-generated client id value to be returned.
     *  Queued messages are sent to the server in order.
     */
    mx_internal function setClientId(value:String):void
    {
        if (_clientId != value)
        {
            var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "clientId", _clientId, value);
            _clientId = value;
            flushClientIdWaitQueue();
            dispatchEvent(event);
        }
    }

    //----------------------------------
    //  connected
    //----------------------------------

    /**
     *  @private
     */
    private var _connected:Boolean = false;

    [Bindable(event="propertyChange")]
    /**
     *  Indicates whether this MessageAgent is currently connected to its
     *  destination via its ChannelSet. The <code>propertyChange</code> event is dispatched when
     *  this property changes.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function get connected():Boolean
    {
        return _connected;
    }

    /**
     *  @private
     */
    protected function setConnected(value:Boolean):void
    {
        if (_connected != value)
        {
            var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "connected", _connected, value);
            _connected = value;
            dispatchEvent(event);
            setAuthenticated(value && channelSet && channelSet.authenticated, _credentials);
        }
    }

    //----------------------------------
    //  destination
    //----------------------------------

    /**
     *  @private
     */
    private var _destination:String = "";

    [Bindable(event="propertyChange")]
    /**
     *  Provides access to the destination for the MessageAgent.
     *  Changing the destination will disconnect the MessageAgent if it is
     *  currently connected.
     *
     *  @throws mx.messaging.errors.InvalidDestinationError If the destination is null or
     *                                  zero-length.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function get destination():String
    {
        return _destination;
    }

    /**
     *  @private
     */
    public function set destination(value:String):void
    {
        if ((value == null) || value.length == 0)
            return; // empty/null destination is checked in internalSend.

        if (_destination != value)
        {
            // If we're using an automatically configured ChannelSet,
            // disconnect from it and null out our ref so we look up the
            // proper configured ChannelSet for the new destination on our next send().
            if ((_channelSetMode == AUTO_CONFIGURED_CHANNELSET) && (channelSet != null))
            {
                channelSet.disconnect(this);
                channelSet = null;
            }

            var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "destination", _destination, value);
            _destination = value;
            dispatchEvent(event);

            if (Log.isInfo())
                _log.info("'{0}' {2} set destination to '{1}'.", id, _destination,  _agentType);
        }
    }

    //----------------------------------
    //  id
    //----------------------------------

    /**
     *  @private
     */
    private var _id:String = UIDUtil.createUID();

    [Bindable(event="propertyChange")]
    /**
     *  @private
     *  The id of this agent.
     */
    public function get id():String
    {
        return _id;
    }

    /**
     *  @private
     */
    public function set id(value:String):void
    {
        if (_id != value)
        {
            var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "id", _id, value);
            _id = value;
            dispatchEvent(event);
        }
    }

    //----------------------------------
    //  requestTimeout
    //----------------------------------

    /**
     *  @private
     */
    private var _requestTimeout:int = -1;

    [Bindable(event="propertyChange")]
    /**
     *  Provides access to the request timeout in seconds for sent messages.
     *  If an acknowledgement, response or fault is not received from the
     *  remote destination before the timeout is reached the message is faulted on the client.
     *  A value less than or equal to zero prevents request timeout.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3           
     */
    public function get requestTimeout():int
    {
        return _requestTimeout;
    }

    /**
     *  @private
     */
    public function set requestTimeout(value:int):void
    {
        if (_requestTimeout != value)
        {
            var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "requestTimeout", _requestTimeout, value);
            _requestTimeout = value;
            dispatchEvent(event);
        }
    }

    //--------------------------------------------------------------------------
    //
    // Internal Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  channelSetMode
    //----------------------------------

    /**
     *  @private
     */
    private var _channelSetMode:int = AUTO_CONFIGURED_CHANNELSET;

    mx_internal function get channelSetMode():int
    {
        return _channelSetMode;
    }

    //----------------------------------
    //  configRequested
    //----------------------------------

    /**
     *  @private
     *  Indicates whether the agent has requested configuration from the server.
     */
    mx_internal var configRequested:Boolean = false;

    //----------------------------------
    //  needsConfig
    //----------------------------------

    /**
     * @private
     */
    private var _needsConfig:Boolean;

    /**
     *  Indicates if this MessageAgent needs to request configuration from the
     *  server.
     */
    mx_internal function get needsConfig():Boolean
    {
        return _needsConfig;
    }

    /**
     *  @private
     */
    mx_internal function set needsConfig(value:Boolean):void
    {
        if (_needsConfig == value)
            return;

        _needsConfig = value;
        if (_needsConfig)
        {
            var cs:ChannelSet = channelSet;
            try
            {
                disconnect();
            }
            finally
            {
                internalSetChannelSet(cs);
            }
        }
    }

    //--------------------------------------------------------------------------
    //
    // Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  Invoked by a MessageResponder upon receiving a result for a sent
     *  message. Subclasses may override this method if they need to perform
     *  custom acknowledgement processing, but must invoke
     *  <code>super.acknowledge()</code> as well. This method dispatches a
     *  MessageAckEvent.
     *
     *  @param ackMsg The AcknowledgMessage returned.
     *
     *  @param msg The original sent message.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function acknowledge(ackMsg:AcknowledgeMessage, msg:IMessage):void
    {
        if (Log.isInfo())
            _log.info("'{0}' {2} acknowledge of '{1}'.", id, msg.messageId, _agentType);

        if (Log.isDebug() && isCurrentChannelNotNull() && getCurrentChannel().mpiEnabled)
        {
            try
            {
                var mpiutil:MessagePerformanceUtils = new MessagePerformanceUtils(ackMsg);
                _log.debug(mpiutil.prettyPrint());
            }
            catch (e:Error)
            {
                _log.debug("Could not get message performance information for: " + msg.toString());
            }
        }

        if (configRequested)
        {
            configRequested = false;
            ServerConfig.updateServerConfigData(ackMsg.body as ConfigMap);
            needsConfig = false;
            if (_pendingConnectEvent)
                channelConnectHandler(_pendingConnectEvent);

            _pendingConnectEvent = null;
        }

        if (clientId == null)
        {
            if (ackMsg.clientId != null)
                setClientId(ackMsg.clientId); // Triggers a call to flush the clientId wait queue.
            else
                flushClientIdWaitQueue();
        }

        dispatchEvent(MessageAckEvent.createEvent(ackMsg, msg));
        monitorRpcMessage(ackMsg,msg);
    }

    /**
     *  Disconnects the MessageAgent's network connection.
     *  This method does not wait for outstanding network operations to complete.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3       
     */
    public function disconnect():void
    {
        if (!_disconnectBarrier)
        {
            // Ensure wait queue for client id value is destroyed.
            _clientIdWaitQueue = null;

            // Only set the barrier used to discard post-disconnect results/faults
            // if the agent is currently connected (otherwise, if this is invoked before
            // connecting and the client fails to connect to the server, no faults will be
            // dispatched).
            if (connected)
                _disconnectBarrier = true;

            if (_channelSetMode == AUTO_CONFIGURED_CHANNELSET)
                internalSetChannelSet(null);
            else if (_channelSet != null)
                _channelSet.disconnect(this);
        }
    }

    /**
     *  Invoked by a MessageResponder upon receiving a fault for a sent message.
     *  Subclasses may override this method if they need to perform custom fault
     *  processing, but must invoke <code>super.fault()</code> as well. This
     *  method dispatchs a MessageFaultEvent.
     *
     *  @param errMsg The ErrorMessage.
     *
     *  @param msg The original sent message that caused this fault.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function fault(errMsg:ErrorMessage, msg:IMessage):void
    {
        if (Log.isError())
            _log.error("'{0}' {2} fault for '{1}'.", id, msg.messageId, _agentType);

        _ignoreFault = false;
        configRequested = false;

        // Remove retryable hint.
        if (errMsg.headers[ErrorMessage.RETRYABLE_HINT_HEADER])
            delete errMsg.headers[ErrorMessage.RETRYABLE_HINT_HEADER];

        if (clientId == null)
        {
            if (errMsg.clientId != null)
                setClientId(errMsg.clientId); // Triggers a call to flush the clientId wait queue.
            else
                flushClientIdWaitQueue();
        }

        dispatchEvent(MessageFaultEvent.createEvent(errMsg));
        monitorRpcMessage(errMsg,msg);

        handleAuthenticationFault(errMsg, msg);
    }

    /**
     *  Handles a CONNECT ChannelEvent. Subclasses that need to perform custom
     *  processing should override this method, and invoke
     *  <code>super.channelConnectHandler()</code>.
     *
     *  @param event The ChannelEvent.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function channelConnectHandler(event:ChannelEvent):void
    {
        _disconnectBarrier = false;
        // If we are waiting on config to come in we can't be connected until
        // we get it. See acknowledge().
        if (needsConfig)
        {
            if (Log.isInfo())
                _log.info("'{0}' {1} waiting for configuration information.", id, _agentType);

            _pendingConnectEvent = event;
        }
        else
        {
            if (Log.isInfo())
                _log.info("'{0}' {1} connected.", id, _agentType);
            setConnected(true);
            dispatchEvent(event);
        }
    }

    /**
     *  Handles a DISCONNECT ChannelEvent. Subclasses that need to perform
     *  custom processing should override this method, and invoke
     *  <code>super.channelDisconnectHandler()</code>.
     *
     *  @param event The ChannelEvent.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function channelDisconnectHandler(event:ChannelEvent):void
    {
        if (Log.isWarn())
            _log.warn("'{0}' {1} channel disconnected.", id, _agentType);
        setConnected(false);
        // If we have remoteCredentials we need to send them on reconnect.
        if (_remoteCredentials != null)
        {
            _sendRemoteCredentials = true;
        }
        dispatchEvent(event);
    }

    /**
     *  Handles a ChannelFaultEvent. Subclasses that need to perform custom
     *  processing should override this method, and invoke
     *  <code>super.channelFaultHandler()</code>.
     *
     *  @param The ChannelFaultEvent
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function channelFaultHandler(event:ChannelFaultEvent):void
    {
        if (Log.isWarn())
            _log.warn("'{0}' {1} channel faulted with {2} {3}", id, _agentType, event.faultCode, event.faultDetail);

        if (!event.channel.connected)
        {
            setConnected(false);
            // If we have remoteCredentials we need to send them on reconnect.
            if (_remoteCredentials != null)
            {
                _sendRemoteCredentials = true;
            }
        }
        dispatchEvent(event);
    }

    /**
     *  Called after the implementing object has been created
     *  and all properties specified on the tag have been assigned.
     *
     *  @param document MXML document that created this object.
     *
     *  @param id id used by the document to refer to this object.
     *  If the object is a deep property on the document, id is null.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function initialized(document:Object, id:String):void
    {
        this.id = id;
    }

    /**
     *  Logs the MessageAgent out from its remote destination.
     *  Logging out of a destination applies to everything connected using the same ChannelSet
     *  as specified in the server configuration. For example, if several DataService components
     *  are connected over an RTMP channel and <code>logout()</code> is invoked on one of them,
     *  all other client components that are connected using the same ChannelSet are also logged out.
     *
     *  <p><b>Note:</b> Adobe recommends that you use the mx.messaging.ChannelSet.logout() method
     *  rather than this method. </p>
     *
     *  @see mx.messaging.ChannelSet#logout()
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function logout():void
    {
        _credentials = null;
        if (channelSet)
            channelSet.logout(this);
    }

    /**
     *  Sets the credentials that the MessageAgent uses to authenticate to
     *  destinations.
     *  The credentials are applied to all services connected over the same ChannelSet.
     *
     *  @param username The username.
     *  @param password The password.
     *  @param charset The character set encoding to use while encoding the
     *  credentials. The default is null, which implies the legacy charset of
     *  ISO-Latin-1. The only other supported charset is &quot;UTF-8&quot;.
     *
     *  @throws flash.errors.IllegalOperationError in two situations; if credentials
     *  have already been set and an authentication is in progress with the remote
     *  detination, or if authenticated and the credentials specified don't match
     *  the currently authenticated credentials.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function setCredentials(username:String, password:String, charset:String=null):void
    {
        if (username == null && password == null)
        {
            _credentials = null;
            _credentialsCharset = null;
        }
        else
        {
            var cred:String = username + ":" + password;
            var encoder:Base64Encoder = new Base64Encoder();
            if (charset == Base64Encoder.CHARSET_UTF_8)
                encoder.encodeUTFBytes(cred);
            else
                encoder.encode(cred);
            _credentials = encoder.drain();
            _credentialsCharset = charset;
        }

        if (channelSet != null)
            channelSet.setCredentials(_credentials, this, _credentialsCharset);
    }

    /**
     *  Sets the remote credentials that will be passed through to the remote destination
     *  for authenticating to secondary systems.
     *
     *  @param username The username.
     *  @param password The password.
     *  @param charset The character set encoding to use while encoding the
     *  remote credentials. The default is null, which implies the legacy
     *  charset of ISO-Latin-1. The only other currently supported option is
     *  &quot;UTF-8&quot;.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    public function setRemoteCredentials(username:String, password:String, charset:String=null):void
    {
        if (username == null && password == null)
        {
            _remoteCredentials = "";
            _remoteCredentialsCharset = null;
        }
        else
        {
            var cred:String = username + ":" + password;
            var encoder:Base64Encoder = new Base64Encoder();
            if (charset == Base64Encoder.CHARSET_UTF_8)
                encoder.encodeUTFBytes(cred);
            else
                encoder.encode(cred);
            _remoteCredentials = encoder.drain();
            _remoteCredentialsCharset = charset;
        }
        _sendRemoteCredentials = true;
    }

    /**
    * Returns true if there are any pending requests for the passed in message.
    * This method should be overriden by subclasses
    *
    * @param msg The message for which the existence of pending requests is checked.
    *
    * @return Returns <code>true</code> if there are any pending requests for the
    * passed in message.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3     
    */
    public function hasPendingRequestForMessage(msg:IMessage):Boolean
    {
        return false;
    }

    //--------------------------------------------------------------------------
    //
    // Internal Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Internal hook for ChannelSet to assign credentials when it has authenticated
     *  successfully via a direct <code>login(...)</code> call to the server or logged
     *  out directly.
     */
    mx_internal function internalSetCredentials(credentials:String):void
    {
        _credentials = credentials;
    }

    //--------------------------------------------------------------------------
    //
    // Protected Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    final protected function assertCredentials(value:String):void
    {
        if (_credentials != null && (_credentials != value))
        {
            var errMsg:ErrorMessage = new ErrorMessage();
            errMsg.faultCode = "Client.Authentication.Error";
            errMsg.faultString = "Credentials specified do not match those used on underlying connection.";
            errMsg.faultDetail = "Channel was authenticated with a different set of credentials than those used for this agent.";
            dispatchEvent(MessageFaultEvent.createEvent(errMsg));
        }
    }

    /**
     *  @private
     *  Utility method to flush any pending queued messages to send once we have
     *  received a clientId from the remote destination.
     */
    final protected function flushClientIdWaitQueue():void
    {
        if (_clientIdWaitQueue != null)
        {
            // If we have a valid clientId, flush all pending messages.
            if (clientId != null)
            {
                while (_clientIdWaitQueue.length > 0)
                {
                    internalSend(_clientIdWaitQueue.shift() as IMessage);
                }
            }

            if (clientId == null)
            {
                // If we still don't have a clientId, remove the first queued message and send it.
                // Leave the queue intact to buffer subsequent sends until we get a response/fault
                // back for this one.
                if (_clientIdWaitQueue.length > 0)
                {
                    var saveQueue:Array = _clientIdWaitQueue;
                    // Make sure we don't just put it back into the queue - we let the first
                    // one through if this is null.
                    _clientIdWaitQueue = null;
                    internalSend(saveQueue.shift() as IMessage);
                    _clientIdWaitQueue = saveQueue;
                }
                else
                {
                    // Regardless of whether the clientId is defined or not, if the wait queue
                    // is empty set it to null to allow the next message to be processed by the
                    // send code path rather than being routed to the queue.
                    _clientIdWaitQueue = null;
                }
            }
        }
    }

    /**
     * Handles the authentication fault on the server. If the authenticated flag is true, 
     * the authentication fault must have been caused by a session expiration on the server.
     * Set the authenticated state to false and if loginAfterDisconnect flag is enabled,
     * resend credentials to the server by disconnecting and resending the message again.
     *
     *  @param errMsg The Error Message.
     *  @param msg The message that caused the fault and should be resent once we have
     *  disconnected/connected causing re-authentication.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    protected function handleAuthenticationFault(errMsg:ErrorMessage, msg:IMessage):void
    {
        if (errMsg.faultCode == "Client.Authentication" && authenticated && isCurrentChannelNotNull())
        {
            var currentChannel:Channel = getCurrentChannel();
            currentChannel.setAuthenticated(false);

            if (currentChannel is PollingChannel && (currentChannel as PollingChannel).loginAfterDisconnect)
            {
                reAuthorize(msg);
                _ignoreFault = true;
            }
        }
    }

    /**
     *  Used to automatically initialize the <code>channelSet</code> property for the
     *  MessageAgent before it connects for the first time.
     *  Subtypes may override to perform custom initialization.
     *
     *  @param message The message that needs to be sent.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    protected function initChannelSet(message:IMessage):void
    {
        if (_channelSet == null)
        {
            _channelSetMode = AUTO_CONFIGURED_CHANNELSET;
            internalSetChannelSet(ServerConfig.getChannelSet(destination));
        }

        if (_channelSet.connected && needsConfig && !configRequested)
        {
            message.headers[CommandMessage.NEEDS_CONFIG_HEADER] = true;
            configRequested = true;
        }

        _channelSet.connect(this);

        if (_credentials != null)
            channelSet.setCredentials(_credentials, this, _credentialsCharset);
    }

    /**
     *  Sends a Message from the MessageAgent to its destination using the
     *  agent's ChannelSet. MessageAgent subclasses must use this method to
     *  send their messages.
     *
     *  @param message The message to send.
     *
     *  @param waitForClientId If true the message may be queued until a clientId has been
     *                         assigned to the agent. In general this is the desired behavior.
     *                         For special behavior (automatic reconnect and resubscribe) the
     *                         agent may pass false to override the default queuing behavior.
     *
     *  @throws mx.messaging.errors.InvalidDestinationError If no destination is set.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3      
     */
    protected function internalSend(message:IMessage, waitForClientId:Boolean = true):void
    {
        // If we don't have a client or server assigned clientId, we
        // need to send a single message and then store any subsequent messages
        // in a buffer to be sent once we've gotten back a server-generated
        // clientId. Otherwise, N outbound messages sent before receiving an ack for
        // the first will result in the generation of N different clientIds in the
        // response/fault messages from the server.
        if ((message.clientId == null) && waitForClientId && (clientId == null))
        {
            if (_clientIdWaitQueue == null)
            {
                _clientIdWaitQueue = [];
                // Current message will be sent but subsequent messages sent before
                // its ack/fault will be queued.
            }
            else
            {
                _clientIdWaitQueue.push(message);
                return; // We've queued the message and will send it once we get a clientId or the outstanding send fails.
            }
        }

        if (message.clientId == null)
            message.clientId = clientId;

        if (requestTimeout > 0)
            message.headers[AbstractMessage.REQUEST_TIMEOUT_HEADER] = requestTimeout;

        if (_sendRemoteCredentials)
        {
            if (! ((message is CommandMessage) &&
                    (CommandMessage(message).operation == CommandMessage.TRIGGER_CONNECT_OPERATION)))
            {
                message.headers[AbstractMessage.REMOTE_CREDENTIALS_HEADER] = _remoteCredentials;
                message.headers[AbstractMessage.REMOTE_CREDENTIALS_CHARSET_HEADER] = _remoteCredentialsCharset;
                _sendRemoteCredentials = false;
            }
        }

        if (channelSet != null)
        {
            if (!connected && (_channelSetMode == MANUALLY_ASSIGNED_CHANNELSET))
                _channelSet.connect(this);

            if (channelSet.connected && needsConfig && !configRequested)
            {
                message.headers[CommandMessage.NEEDS_CONFIG_HEADER] = true;
                configRequested = true;
            }

            channelSet.send(this, message);
            monitorRpcMessage(message,message);
        }
        else if (destination != null && destination.length > 0)
        {
            initChannelSet(message);
            if (channelSet != null)
            {
                channelSet.send(this, message);
                monitorRpcMessage(message,message);
            }
        }
        else
        {
            var msg:String = resourceManager.getString(
                "messaging", "destinationNotSet");
            throw new InvalidDestinationError(msg);
        }
    }

    /**
     * This function should be overriden by sublasses to implement reauthentication due to
     * server session time-out behavior specific to them. In general, it should follow disconnect, 
     * connect, resend message pattern.
     *
     *  @param msg The message that caused the fault and should be resent once we have
     *  disconnected/connected causing reauthentication.
     *
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3
     */
    protected function reAuthorize(msg:IMessage):void
    {
        // Disconnect all message agents from the Channel to make sure the Channel
        // is fully disconnected and Channel#internalConnect gets called which 
        // sends the login command to reauthenticate the Channel.
        if (channelSet != null)
            channelSet.disconnectAll();
        internalSend(msg);
    }

    /**
     *  @private
     */
    private function getCurrentChannel():Channel
    {
        return channelSet != null? channelSet.currentChannel : null;
    }

    /**
     *  @private
     */
    private function isCurrentChannelNotNull():Boolean
    {
        return getCurrentChannel() != null;
    }

    /**
     * Monitor a rpc message that is being send
     */
    private function monitorRpcMessage(message:IMessage,actualMessage:IMessage):void
    {
        if (NetworkMonitor.isMonitoring())
        {
            if (message is ErrorMessage)
            {
                NetworkMonitor.monitorFault(actualMessage, MessageFaultEvent.createEvent(ErrorMessage(message)));
            }
            else if (message is AcknowledgeMessage)
            {
                NetworkMonitor.monitorResult(message, MessageEvent.createEvent(MessageEvent.RESULT, actualMessage));
            }
            else
            {
                NetworkMonitor.monitorInvocation(getNetmonId(), message, this);
            }
        }
    }

    /**
     * Return the id for the NetworkMonitor.
     * @private
     */
    mx_internal function getNetmonId():String
    {
        return null;
    }
}

}
