# Copyright (c) 2007-2009 The PyAMF Project.
# See LICENSE for details.

"""
AMF Remoting support.

A Remoting request from the client consists of a short preamble, headers, and
bodies. The preamble contains basic information about the nature of the
request. Headers can be used to request debugging information, send
authentication info, tag transactions, etc. Bodies contain actual Remoting
requests and responses. A single Remoting envelope can contain several
requests; Remoting supports batching out of the box.

Client headers and bodies need not be responded to in a one-to-one manner. That
is, a body or header may not require a response. Debug information is requested
by a header but sent back as a body object. The response index is essential for
the Flash Player to understand the response therefore.

@see: U{Remoting Envelope on OSFlash (external)
<http://osflash.org/documentation/amf/envelopes/remoting>}
@see: U{Remoting Headers on OSFlash (external)
<http://osflash.org/amf/envelopes/remoting/headers>}
@see: U{Remoting Debug Headers on OSFlash (external)
<http://osflash.org/documentation/amf/envelopes/remoting/debuginfo>}

@since: 0.1.0
"""

import copy

import pyamf
from pyamf import util

__all__ = ['Envelope', 'Request', 'Response', 'decode', 'encode']

#: Succesful call.
STATUS_OK = 0
#: Reserved for runtime errors.
STATUS_ERROR = 1
#: Debug information.
STATUS_DEBUG = 2

#: List of available status response codes.
STATUS_CODES = {
    STATUS_OK:    '/onResult',
    STATUS_ERROR: '/onStatus',
    STATUS_DEBUG: '/onDebugEvents'
}

#: AMF mimetype.
CONTENT_TYPE = 'application/x-amf'

ERROR_CALL_FAILED, = range(1)
ERROR_CODES = {

    ERROR_CALL_FAILED: 'Server.Call.Failed'
}

APPEND_TO_GATEWAY_URL = 'AppendToGatewayUrl'
REPLACE_GATEWAY_URL = 'ReplaceGatewayUrl'
REQUEST_PERSISTENT_HEADER = 'RequestPersistentHeader'

class RemotingError(pyamf.BaseError):
    """
    Generic remoting error class.
    """

class RemotingCallFailed(RemotingError):
    """
    Raised 
if Server.Call.Failed received
    """

pyamf.add_error_class(RemotingCallFailed, ERROR_CODES[ERROR_CALL_FAILED])

class HeaderCollection(dict):
    """
    Collection of AMF message headers.
    """
    def __init__(self, raw_headers={}):
        self.required = []

        for (k, ig, v) in raw_headers:
            self[k] = v
            if ig:
                self.required.append(k)

    def is_required(self, idx):
        """
        @raise KeyError: Unknown header found.
        """
        if not idx in self:
            raise KeyError("Unknown header %s" % str(idx))

        return idx in self.required

    def set_required(self, idx, value=True):
        """
        @raise KeyError: Unknown header found.
        """
        if not idx in self:
            raise KeyError("Unknown header %s" % str(idx))

        if not idx in self.required:
            self.required.append(idx)

    def __len__(self):
        return len(self.keys())

class Envelope(object):
    """
    I wrap an entire request, encapsulating headers and bodies.

    There can be more than one request in a single transaction.

    @ivar amfVersion: AMF encoding version. See L{pyamf.ENCODING_TYPES}
    @type amfVersion: C{int} or C{None}
    @ivar clientType: Client type. See L{ClientTypes<pyamf.ClientTypes>}
    @type clientType: C{int} or C{None}
    @ivar headers: AMF headers, a list of name, value pairs. Global to each
        request.
    @type headers: L{HeaderCollection}
    @ivar bodies: A list of requests/response messages
    @type bodies: L{list} containing tuples of the key of the request and
        the instance of the L{Message}
    """
    def __init__(self, amfVersion=None, clientType=None):
        self.amfVersion = amfVersion
        self.clientType = clientType
        self.headers = HeaderCollection()
        self.bodies = []

    def __repr__(self):
        r = "<Envelope amfVersion=%s clientType=%s>\n" % (
            self.amfVersion, self.clientType)

        for h in self.headers:
            r += " " + repr(h) + "\n"

        for request in iter(self):
            r += " " + repr(request) + "\n"

        r += "</Envelope>"

        return r

    def __setitem__(self, name, value):
        if not isinstance(value, Message):
            raise TypeError("Message instance expected")

        idx = 0
        found = False

        for body in self.bodies:
            if name == body[0]:
                self.bodies[idx] = (name, value)
                found = True

            idx = idx + 1

        if not found:
            self.bodies.append((name, value))

        value.envelope = self

    def __getitem__(self, name):
        for body in self.bodies:
            if name == body[0]:
                return body[1]

        raise KeyError("'%r'" % (name,))

    def __iter__(self):
        for body in self.bodies:
            yield body[0], body[1]

        raise StopIteration

    def __len__(self):
        return len(self.bodies)

    def iteritems(self):
        for body in self.bodies:
            yield body

        raise StopIteration

    def keys(self):
        return [body[0] for body in self.bodies]

    def items(self):
        return self.bodies

    def __contains__(self, name):
        for body in self.bodies:
            if name == body[0]:
                return True

        return False

    def __eq__(self, other):
        if isinstance(other, Envelope):
            return self.amfVersion == other.amfVersion and \
                self.clientType == other.clientType and \
                self.headers == other.headers and \
                self.bodies == other.bodies

        if hasattr(other, 'keys') and hasattr(other, 'items'):
            keys, o_keys = self.keys(), other.keys()

            if len(o_keys) != len(keys):
                return False

            for k in o_keys:
                if k not in keys:
                    return False

                keys.remove(k)

            for k, v in other.items():
                if self[k] != v:
                    return False

            return True

class Message(object):
    """
    I represent a singular request/response, containing a collection of
    headers and one body of data.

    I am used to iterate over all requests in the L{Envelope}.

    @ivar envelope: The parent envelope of this AMF Message.
    @type envelope: L{Envelope}
    @ivar body: The body of the message.
    @type body: C{mixed}
    @ivar headers: The message headers.
    @type headers: C{dict}
    """
    def __init__(self, envelope, body):
        self.envelope = envelope
        self.body = body

    def _get_headers(self):
        return self.envelope.headers

    headers = property(_get_headers)

class Request(Message):
    """
    An AMF Request payload.

    @ivar target: The target of the request
    @type target: C{basestring}
    """
    def __init__(self, target, body=[], envelope=None):
        Message.__init__(self, envelope, body)

        self.target = target

    def __repr__(self):
        return "<%s target=%s>%s</%s>" % (
            type(self).__name__, self.target, self.body, type(self).__name__)

class Response(Message):
    """
    An AMF Response.

    @ivar status: The status of the message. Default is L{STATUS_OK}.
    @type status: Member of L{STATUS_CODES}.
    """
    def __init__(self, body, status=STATUS_OK, envelope=None):
        Message.__init__(self, envelope, body)

        self.status = status

    def __repr__(self):
        return "<%s status=%s>%s</%s>" % (
            type(self).__name__, _get_status(self.status), self.body,
            type(self).__name__
        )

class BaseFault(object):
    """
    I represent a C{Fault} message (C{mx.rpc.Fault}).

    @ivar level: The level of the fault.
    @type level: C{str}
    @ivar code: A simple code describing the fault.
    @type code: C{str}
    @ivar details: Any extra details of the fault.
    @type details: C{str}
    @ivar description: Text description of the fault.
    @type description: C{str}

    @see: U{mx.rpc.Fault on Livedocs (external)
    <http://livedocs.adobe.com/flex/201/langref/mx/rpc/Fault.html>}
    """
    level = N