"""Implements the SOAP protocol. Specifically, this implementation talks in
the wrapped document/literal form."""

import re
import sys
import traceback
import time
import logging

import dispatch
import cherrypy
import cElementTree as et
from genshi.builder import tag, Element, Namespace

from turbogears import controllers, expose as tgexpose, error_handler, \
                       exception_handler, validators

from tgwebservices.runtime import primitives, ctvalues, typedproperty, unsigned

log = logging.getLogger("tgwebservices.soap")

namespace_expr = re.compile(r'^\{.*\}')

_simplevalues = set([int, long, basestring, str, unicode, float])

@dispatch.generic()
def soap_value(name, return_type, value):
    """Converts a value into xml Element objects for inclusion in the
    SOAP response output."""
    pass

@soap_value.when("type(value) in _simplevalues")
def soapint(name, return_type, value):
    elem = Element(name, **{"xsi:type" : soap_type(return_type)})
    elem(str(value))
    return elem

@soap_value.when("isinstance(value, bool)")
def soapbool(name, return_type, value):
    elem = Element(name, **{"xsi:type" : "xsd:boolean"})
    elem(str(value).lower())
    return elem

@soap_value.when("type(value) not in primitives")
def soap_instance(name, return_type, value):
    """Handles an instance of a complex type."""
    elem = Element(name, **{"xsi:type" : value.__class__.__name__})
    cls = value.__class__
    for key in ctvalues(cls):
        elem.append(soap_value(key, getattr(cls, key),
                    getattr(value, key)))
    return elem

@soap_value.when("isinstance(value, list)")
def soap_list(name, return_type, value):
    # in the context of the SOAP output, the types: namespace is implied
    elem = Element(name, **{"xsi:type" : 
            soap_type(return_type).replace("types:", "")})
    itemtype = soap_type(return_type)
    for item in value:
        elem.append(soap_value("item", itemtype, item))
    return elem

def soap_body(methodname, function_info, output):
    """Generates xml Elements for the body of a SOAP response."""
    body = Element(methodname + "Response")
    body.append(soap_value("result", function_info.return_type, output))
    return body


# Maps Python types to XML Schema types
type_registry = {
    basestring : "xsd:string",
    str : "xsd:string",
    unicode : "xsd:string",
    int : "xsd:int",
    long : "xsd:long",
    float : "xsd:float",
    bool : "xsd:boolean",
    unsigned : "xsd:unsignedInt"
}

# Maps python types to appropriately named array types.
array_registry = {
    basestring : "String_Array",
    str : "String_Array",
    unicode : "String_Array",
    int : "Int_Array",
    long : "Long_Array",
    float : "Float_Array",
    bool : "Boolean_Array",
}

def handle_list(kind, arrays):
    """Figures out what kind of array the user has declared and registers
    that type. Arrays need to be declared separately in the XML schema."""
    # check for primitive types
    if kind in array_registry:
        arraytype = array_registry[kind]
    elif not isinstance(kind, type):
        return handle_list(kind.__class__, arrays)
    else:
        arraytype = kind.__name__ + "_Array"
    if arrays is not None:
        arrays.add(kind)
    return arraytype
        

def soap_type(kind, arrays=None):
    """Figures out an appropriate schema declaration for the object that is 
    passed in."""
    if kind is None:
        return "xsd:string"
    
    if isinstance(kind, typedproperty):
        return soap_type(kind.type, arrays)
        
    if isinstance(kind, list):
        try:
            return "types:" + handle_list(kind[0], arrays)
        except IndexError:
            raise IndexError("Cannot determine SOAP data type from empty list")
    if kind in type_registry:
        return type_registry.get(kind)
    if not isinstance(kind, type):
        return soap_type(kind.__class__, arrays)
    return "types:" + kind.__name__

class SoapController(controllers.Controller):
    """Controller that manages the SOAP requests and responses. This also
    provides the api.wsdl URL that describes the web service."""
    def __init__(self, wscontroller, tns, typenamespace):
        self.wscontroller = wscontroller
        self.arrays = None
        self.tns = tns
        self.typenamespace = typenamespace
    
    def _cp_on_error(self):
        e = sys.exc_info()[1]
        soap = Namespace("http://schemas.xmlsoap.org/soap/envelope/")
        
        # set up elements common to all faults
        envelope = Element(soap.Envelope)
        body = Element("Body")
        envelope.append(body)
        fault = Element("Fault")
        body.append(fault)
        faultcode = Element("faultcode")
        fault.append(faultcode)
        faultstring = Element("faultstring")
        faultstring.append(str(e))
        fault.append(faultstring)
        
        cherrypy.response.headers["Content-Type"] = "text/xml;charset=utf-8"
        
        # customize the fault for the type of problem
        if isinstance(e, validators.Invalid):
            cherrypy.response.status = "500 Invalid Input"
            faultcode.append("Client")
        else:
            cherrypy.response.status = "500 Internal Server Error"
            faultcode.append("Server")
            detail = Element("detail")
            fault.append(detail)
            method = getattr(cherrypy.request, "soap_method", "")
            data = getattr(cherrypy.request, "soap_start", "")
            detail.append("Error occurred at %s\n" % (time.asctime()))
            detail.append("SOAP method: %s\n" % method)
            detail.append("\nRaw request (first 2K):\n%s\n" % data)
            detail.append("\nTraceback:\n%s\n" % 
                "\n".join(traceback.format_exception(*sys.exc_info())))
            
        cherrypy.response.body = str(envelope)
    
    @tgexpose("genshi:tgwebservices.templates.soap",
              content_type="text/xml;charset=utf-8", format="xml")
    def index(self):
        """Processes SOAP requests and generates the responses."""
        # get request data and produce an ElementTree that we can work with.
        request = cherrypy.request
        response = cherrypy.response
        clen = int(request.headers.get('Content-Length')) or 0
        data = request.body.read(clen)
        request.soap_start = data[:2048]
        soapreq = et.fromstring(data)
        
        # find the body of the request and the specific method name that has
        # been requested.
        body = soapreq.find("{http://schemas.xmlsoap.org/soap/envelope/}Body")
        body = body.getchildren()[0]
        methodname = body.tag
        methodname = namespace_expr.sub("", methodname)
        request.soap_method = methodname
        
        try:
            method = self.wscontroller._ws_funcs[methodname]
        except KeyError:
            raise validators.Invalid("Unknown SOAP method: %s" %
                                     methodname, methodname, None)
        
        params = {"_tgws" : True}
        params["xml_body"] = body
        
        # ensure that the requested method has actually been exposed
        assert hasattr(method, "exposed") and method.exposed == True
        
        # set the _tgws flag so that the TurboGears wrapping is not applied
        # in wsexpose. We want to manage the produced dictionary here, because
        # we need to pass additional information on to the soap template.
        output = method(**params)
        data = dict(baseURL=self.wscontroller._ws_baseURL,
                    tns=self.tns,
                    typenamespace=self.typenamespace,
                    soap_body=soap_body, output=output,
                    methodname=methodname, 
                    function_info =
                        self.wscontroller._ws_funcs[methodname]._ws_func_info)
        return data
    
    @tgexpose("genshi:tgwebservices.templates.wsdl", format="xml",
              content_type="text/xml;charset=utf-8")
    def api_wsdl(self, xmlsoap=False):
        """Produces the WSDL for this SOAP service."""
        if self.arrays is None:
            self._initialize_arrays()
        ct = list(self.wscontroller._ws_complex_types)
        ct.sort(key=lambda item: item._type_dependents, reverse=True)
        funclist = self.wscontroller._ws_funcs.keys()
        funclist.sort()
        
        # Apache Axis2 demands the w3.org namespace for soapenc
        # Axis1 needs the xmlsoap variety.
        if xmlsoap:
            soapenc = "http://schemas.xmlsoap.org/soap/encoding/"
        else:
            soapenc = "http://www.w3.org/2001/09/soap-encoding"
        return dict(service_name=self.wscontroller.__class__.__name__,
                    baseURL=self.wscontroller._ws_baseURL,
                    registry=self.wscontroller._ws_funcs, soap_type=soap_type,
                    funclist=funclist,
                    soap_array=handle_list,
                    complex_types=ct,
                    ctvalues=ctvalues,
                    arrays=self.arrays, soapenc=soapenc,
                    tns=self.tns, typenamespace=self.typenamespace)
    
    @tgexpose()
    def xmlsoap_wsdl(self):
        return self.api_wsdl(xmlsoap=True)
    
    def _initialize_arrays(self):
        arrays = set()
        for func in self.wscontroller._ws_funcs.values():
            soap_type(func._ws_func_info.return_type, arrays)
        for cls in self.wscontroller._ws_complex_types:
            for key in ctvalues(cls):
                soap_type(getattr(cls, key), arrays)
        self.arrays = arrays
