# -*- coding: utf-8 -*-
#
# papyon - a python client library for Msn
#
# Copyright (C) 2005-2007 Ali Sabil <ali.sabil@gmail.com>
# Copyright (C) 2007 Johann Prieur <johann.prieur@gmail.com>
# Copyright (C) 2010 Collabora Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import description
from errors import SOAPParseError
from SOAPUtils import *
from papyon.gnet.errors import HTTPError
from papyon.util.async import *

import papyon.gnet.protocol
import papyon.util.element_tree as ElementTree
import papyon.util.string_io as StringIO
import re
import logging

__all__ = ['SOAPService', 'SOAPResponse']

logger = logging.getLogger('papyon.service')

def url_split(url, default_scheme='http'):
    from urlparse import urlsplit, urlunsplit
    if "://" not in url: # fix a bug in urlsplit
        url = default_scheme + "://" + url
    protocol, host, path, query, fragment = urlsplit(url)
    if path == "": path = "/"
    try:
        host, port = host.rsplit(":", 1)
        port = int(port)
    except:
        port = None
    resource = urlunsplit(('', '', path, query, fragment))
    return protocol, host, port, resource

def compress_xml(xml_string):
    space_regex = [(re.compile('>\s+<'), '><'),
        (re.compile('>\s+'), '>'),
        (re.compile('\s+<'), '<')]

    for regex, replacement in space_regex:
        xml_string = regex.sub(replacement, xml_string)
    return xml_string

soap_template = """<?xml version='1.0' encoding='utf-8'?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Header>
        %s
    </soap:Header>
    <soap:Body>
        %s
    </soap:Body>
</soap:Envelope>"""

class SOAPFault(object):
    def __init__(self, tree):
        self.tree = tree
        self.faultcode = None
        self.faultstring = None
        self.faultactor = None
        self.detail = None

        if tree is not None:
            self.faultcode = tree.findtext("./faultcode")
            self.faultstring = tree.findtext("./faultstring")
            self.faultactor = tree.findtext("./faultactor")
            self.detail = tree.find("./detail")

    def is_fault(self):
        return self.tree is not None

    def __repr__(self):
        return """	fault code : %s
	fault string : %s
	fault actor : %s
	detail : %s""" % (
            self.faultcode, self.faultstring, self.faultactor, self.detail)

    def __str__(self):
        return self.__repr__()


class SOAPResponse(ElementTree.XMLResponse):
    NS_SHORTHANDS = {"soap" : XMLNS.SOAP.ENVELOPE,
            "xmlenc" : XMLNS.ENCRYPTION.BASE,
            "wsse" : XMLNS.WS.SECEXT,
            "wst" : XMLNS.WS.TRUST,
            "wsa" : XMLNS.WS.ADDRESSING,
            "wsp" : XMLNS.WS.POLICY,
            "wsi" : XMLNS.WS.ISSUE,
            "wsu" : XMLNS.WS.UTILITY,
            "ps" : XMLNS.MICROSOFT.PASSPORT,
            "psf" : XMLNS.MICROSOFT.PASSPORT_FAULT,
            "ab" : XMLNS.MICROSOFT.LIVE.ADDRESSBOOK,
            "st" : XMLNS.MICROSOFT.LIVE.STORAGE,
            "stv1" : XMLNS.MICROSOFT.LIVE.STORAGEV1,
            "oim" : XMLNS.MICROSOFT.LIVE.OIM,
            "rsi" : XMLNS.MICROSOFT.LIVE.RSI,
            "spaces" : XMLNS.MICROSOFT.LIVE.SPACES }

    def __init__(self, soap_data):
        ElementTree.XMLResponse.__init__(self, soap_data, self.NS_SHORTHANDS)
        try:
            self.header = self.tree.find("./soap:Header")
            self.body = self.tree.find("./soap:Body")
            try:
                self.fault = SOAPFault(self.body.find("./soap:Fault"))
            except:
                self.fault = SOAPFault(self.tree.find("./soap:Fault"))
        except:
            raise SOAPParseError("invalid xml+soap data", soap_data)

        if not self.is_valid():
            raise SOAPParseError("no header, fault or body", soap_data)

    def is_fault(self):
        return self.fault.is_fault()

    def is_valid(self):
        return ((self.header is not None) or \
                (self.fault is not None) or \
                (self.body is not None)) \
            and self.tree is not None

    def _parse(self, data):
        events = ("start", "end", "start-ns", "end-ns")
        ns = []
        data = StringIO.StringIO(data)
        context = ElementTree.iterparse(data, events=events)
        for event, elem in context:
            if event == "start-ns":
                ns.append(elem)
            elif event == "end-ns":
                ns.pop()
            elif event == "start":
                elem.set("(xmlns)", tuple(ns))
        data.close()
        return context.root

class SOAPService(object):

    def __init__(self, name, proxies=None):
        self._name = name
        self._service = getattr(description, self._name)
        self._active_transports = {}
        self._proxies = proxies or {}

        # Regex to find password
        self.password_regex = re.compile("<wsse:Password>.*?</wsse:Password>", re.S)

    def _send_request(self, name, url, soap_header, soap_body, soap_action,
            callback, errback=None, transport_headers={}, user_data=None):

        scheme, host, port, resource = url_split(url)
        http_headers = transport_headers.copy()
        if soap_action is not None:
            http_headers["SOAPAction"] = str(soap_action)
        http_headers["Content-Type"] = "text/xml; charset=utf-8"
        http_headers["Cache-Control"] = "no-cache"
        if "Accept" not in http_headers:
            http_headers["Accept"] = "text/*"
        http_headers["Proxy-Connection"] = "Keep-Alive"
        http_headers["Connection"] = "Keep-Alive"
        http_headers["Accept-Encoding"] = "gzip"

        request = compress_xml(soap_template % (soap_header, soap_body))

        transport = self._get_transport(name, scheme, host, port,
                callback, errback, user_data)
        transport.request(resource, http_headers, request, 'POST')

    def _soap_request(self, method, header_args, body_args, callback, errback,
            user_data=None):
        http_headers = method.transport_headers()
        soap_action = method.soap_action()

        soap_header = method.soap_header(*header_args)
        soap_body = method.soap_body(*body_args)

        method_name = method.__name__.rsplit(".", 1)[1]
        self._send_request(method_name, self._service.url, soap_header,
                soap_body, soap_action, callback, errback, http_headers,
                user_data)

    def _response_handler(self, transport, http_response):
        request = self._unref_transport(transport)
        if request is None:
            logger.warning("No active request for HTTP response received")
            return
        request_id, callback, errback, user_data = request
        method = getattr(self._service, request_id)

        # decode, build and process SOAP response
        try:
            logger.debug("<<< Received response for %s" % request_id)
            decoded_body = http_response.decode_body()
            logger.debug("<<<" + unicode(decoded_body, "utf-8"))
            soap_response = SOAPResponse(decoded_body)
            if not soap_response.is_fault():
                response = method.process_response(soap_response)
                if not response:
                    raise SOAPParseError("response wasn't found", decoded_body)
        except Exception, err:
            logger.exception(err)
            logger.error("Couldn't build or process SOAP response")
            run(errback, err)
            return

        # handle SOAP response or fault
        if not soap_response.is_fault():
            handler = getattr(self, "_Handle" + request_id + "Response", None)
            if handler is not None:
                handler(callback, errback, response, user_data)
            else:
                self._HandleSOAPResponse(request_id, callback, errback,
                        response, user_data)
        else:
            handler = getattr(self, "_Handle" + request_id + "Fault", None)
            if handler is not None:
                if handler(callback, errback, soap_response, user_data):
                    return # if handler returns true, don't call generic handler
            self._HandleSOAPFault(request_id, callback, errback, soap_response,
                                  user_data)

    def _request_handler(self, transport, http_request):
        #hide password from logs
        cleaned = self.password_regex.sub("<wsse:Password>*****</wsse:Password>", unicode(http_request))

        logger.debug(">>> " + unicode(cleaned))

    def _error_handler(self, transport, error):
        logger.warning("Transport Error : " + str(error))

        # try to process response if we received an HTTP error (status != 2xx)
        if isinstance(error, HTTPError):
            self._response_handler(transport, error.response)
            return

        # transport probably died, dispose all requests on it
        for request in self._dispose_transport(transport):
            request_id, callback, errback, user_data = request
            run(errback, error)

    # Handlers
    def _HandleSOAPFault(self, request_id, callback, errback,
            soap_response, user_data):
        logger.warning("Unhandled SOAPFault to %s" % request_id)

    def _HandleSOAPResponse(self, request_id, callback, errback,
            response, user_data):
        logger.warning("Unhandled Response to %s" % request_id)

    # Transport management
    def _get_transport(self, request_id, scheme, host, port,
            callback, errback, user_data):
        key = (scheme, host, port)
        if key in self._active_transports:
            trans = self._active_transports[key]
            transport = trans[0]
            trans[1].append((request_id, callback, errback, user_data))
        else:
            transport = papyon.gnet.protocol.ProtocolFactory(scheme,
                    host, port, proxies=self._proxies)
            handler_id = [
                transport.connect("response-received", self._response_handler),
                transport.connect("request-sent", self._request_handler),
                transport.connect("error", self._error_handler)]

            trans = [transport, [(request_id, callback, errback, user_data)], handler_id]
            self._active_transports[key] = trans
        return transport

    def _unref_transport(self, transport):
        for key, trans in self._active_transports.iteritems():
            if trans[0] == transport:
                response = trans[1].pop(0)

                if len(trans[1]) != 0:
                    return response

                for handle in trans[2]:
                    transport.disconnect(handle)
                del self._active_transports[key]
                return response
        return None

    def _dispose_transport(self, transport):
        for key, trans in self._active_transports.iteritems():
            if trans[0] == transport:
                response = trans[1]
                for handle in trans[2]:
                    transport.disconnect(handle)
                del self._active_transports[key]
                return response
        return []
