# a port of xmlrpclib to json....
#
#
# The JSON-RPC client interface is based on the XML-RPC client
#
# Copyright (c) 1999-2002 by Secret Labs AB
# Copyright (c) 1999-2002 by Fredrik Lundh
# Copyright (c) 2006 by Matt Harrison
#
# By obtaining, using, and/or copying this software and/or its
# associated documentation, you agree that you have read, understood,
# and will comply with the following terms and conditions:
#
# Permission to use, copy, modify, and distribute this software and
# its associated documentation for any purpose and without fee is
# hereby granted, provided that the above copyright notice appears in
# all copies, and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of
# Secret Labs AB or the author not be used in advertising or publicity
# pertaining to distribution of the software without specific, written
# prior permission.
#
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
# ABILITY AND FITNESS.  IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
# OF THIS SOFTWARE.
# --------------------------------------------------------------------
import base64
import json
from http.client import HTTPConnection, HTTPSConnection
from urllib.parse import splithost, splittype, splituser, unquote


__version__ = '0.0.1'

ID = 1


def _gen_id():
    global ID
    ID = ID + 1
    return ID


# --------------------------------------------------------------------
# Exceptions

##
# Base class for all kinds of client-side errors.


class Error(Exception):
    """Base class for client errors."""

    def __str__(self):
        return repr(self)


##
# Indicates an HTTP-level protocol error.  This is raised by the HTTP
# transport layer, if the server returns an error code other than 200
# (OK).
#
# @param url The target URL.
# @param errcode The HTTP error code.
# @param errmsg The HTTP error message.
# @param headers The HTTP header dictionary.


class ProtocolError(Error):
    """Indicates an HTTP protocol error."""

    def __init__(self, url, errcode, errmsg, headers, response):
        Error.__init__(self)
        self.url = url
        self.errcode = errcode
        self.errmsg = errmsg
        self.headers = headers
        self.response = response

    def __repr__(self):
        return '<ProtocolError for %s: %s %s>' % (self.url, self.errcode, self.errmsg)


def getparser(encoding):
    un = Unmarshaller(encoding)
    par = Parser(un)
    return par, un


def dumps(params, methodname=None, methodresponse=None, encoding=None, allow_none=0):
    if methodname:
        request = {}
        request['method'] = methodname
        request['params'] = params
        request['id'] = _gen_id()
        return json.dumps(request)
    return None


class Unmarshaller:
    def __init__(self, encoding):
        self.data = None
        self.encoding = encoding

    def feed(self, data):
        if self.data is None:
            self.data = data
        else:
            self.data = self.data + data

    def close(self):
        # try to convert string to json
        return json.loads(self.data.decode(self.encoding))


class Parser:
    def __init__(self, unmarshaller):
        self._target = unmarshaller
        self.data = None

    def feed(self, data):
        if self.data is None:
            self.data = data
        else:
            self.data = self.data + data

    def close(self):
        self._target.feed(self.data)


class _Method:
    # some magic to bind an JSON-RPC method to an RPC server.
    # supports "nested" methods (e.g. examples.getStateName)

    def __init__(self, send, name):
        self.__send = send
        self.__name = name

    def __getattr__(self, name):
        return _Method(self.__send, f'{self.__name}.{name}')

    def __call__(self, *args):
        return self.__send(self.__name, args)


##
# Standard transport class for JSON-RPC over HTTP.
# <p>
# You can create custom transports by subclassing this method, and
# overriding selected methods.


class Transport:
    """Handles an HTTP transaction to an JSON-RPC server."""

    # client identifier (may be overridden)
    user_agent = 'jsonlib.py/%s (by matt harrison)' % __version__

    ##
    # Send a complete request, and parse the response.
    #
    # @param host Target host.
    # @param handler Target PRC handler.
    # @param request_body JSON-RPC request body.
    # @param verbose Debugging flag.
    # @return Parsed response.

    def request(self, host, handler, request_body, encoding, verbose=0):
        # issue JSON-RPC request

        h = self.make_connection(host)
        if verbose:
            h.set_debuglevel(1)

        self.send_request(h, handler, request_body)
        self.send_user_agent(h)
        self.send_content(h, request_body)

        try:
            errcode, errmsg, headers = h.getreply()
            r = h.getfile()
        except AttributeError:
            r = h.getresponse()
            errcode = r.status
            errmsg = r.reason
            headers = r.getheaders()

        if errcode != 200:
            response = r.read()
            raise ProtocolError(
                host + handler,
                errcode,
                errmsg,
                headers,
                response,
            )

        self.verbose = verbose

        try:
            sock = h._conn.sock
        except AttributeError:
            sock = None

        return self._parse_response(r, sock, encoding)

    ##
    # Create parser.
    #
    # @return A 2-tuple containing a parser and a unmarshaller.

    def getparser(self, encoding):
        # get parser and unmarshaller
        return getparser(encoding)

    ##
    # Get authorization info from host parameter
    # Host may be a string, or a (host, x509-dict) tuple; if a string,
    # it is checked for a "user:pw@host" format, and a "Basic
    # Authentication" header is added if appropriate.
    #
    # @param host Host descriptor (URL or (URL, x509 info) tuple).
    # @return A 3-tuple containing (actual host, extra headers,
    #     x509 info).  The header and x509 fields may be None.

    def get_host_info(self, host):
        x509 = {}
        if isinstance(host, tuple):
            host, x509 = host

        auth, host = splituser(host)

        if auth:
            auth = base64.encodestring(unquote(auth))
            auth = ''.join(auth.split())  # get rid of whitespace
            extra_headers = [
                ('Authorization', 'Basic ' + auth),
            ]
        else:
            extra_headers = None

        return host, extra_headers, x509

    ##
    # Connect to server.
    #
    # @param host Target host.
    # @return A connection handle.

    def make_connection(self, host):
        # create a HTTP connection object from a host descriptor
        host, _extra_headers, _x509 = self.get_host_info(host)
        return HTTPConnection(host)

    ##
    # Send request header.
    #
    # @param connection Connection handle.
    # @param handler Target RPC handler.
    # @param request_body JSON-RPC body.

    def send_request(self, connection, handler, request_body):
        connection.putrequest('POST', handler)

    ##
    # Send host name.
    #
    # @param connection Connection handle.
    # @param host Host name.

    def send_host(self, connection, host):
        host, extra_headers, _x509 = self.get_host_info(host)
        connection.putheader('Host', host)
        if extra_headers:
            if isinstance(extra_headers, dict):
                extra_headers = list(extra_headers.items())
            for key, value in extra_headers:
                connection.putheader(key, value)

    ##
    # Send user-agent identifier.
    #
    # @param connection Connection handle.

    def send_user_agent(self, connection):
        connection.putheader('User-Agent', self.user_agent)

    ##
    # Send request body.
    #
    # @param connection Connection handle.
    # @param request_body JSON-RPC request body.

    def send_content(self, connection, request_body):
        connection.putheader('Content-Type', 'text/xml')
        connection.putheader('Content-Length', str(len(request_body)))
        connection.endheaders()
        if request_body:
            connection.send(request_body)

    ##
    # Parse response.
    #
    # @param file Stream.
    # @return Response tuple and target method.

    def parse_response(self, file):
        # compatibility interface
        return self._parse_response(file, None)

    ##
    # Parse response (alternate interface).  This is similar to the
    # parse_response method, but also provides direct access to the
    # underlying socket object (where available).
    #
    # @param file Stream.
    # @param sock Socket handle (or None, if the socket object
    #    could not be accessed).
    # @return Response tuple and target method.

    def _parse_response(self, file, sock, encoding):
        # read response from input file/socket, and parse it

        p, u = self.getparser(encoding)

        while 1:
            response = sock.recv(1024) if sock else file.read(1024)
            if not response:
                break
            if self.verbose:
                print('body:', repr(response))
            p.feed(response)

        file.close()
        p.close()

        return u.close()


##
# Standard transport class for JSON-RPC over HTTPS.


class SafeTransport(Transport):
    """Handles an HTTPS transaction to an JSON-RPC server."""

    # FIXME: mostly untested

    def make_connection(self, host):
        # create a HTTPS connection object from a host descriptor
        # host may be a string, or a (host, x509-dict) tuple
        host, _extra_headers, x509 = self.get_host_info(host)
        try:
            HTTPS = HTTPSConnection
        except AttributeError:
            raise NotImplementedError(
                "your version of httplib doesn't support HTTPS",
            )
        else:
            return HTTPS(host, None, **(x509 or {}))


class ServerProxy:
    def __init__(self, uri, transport=None, encoding=None, verbose=None, allow_none=0):
        utype, uri = splittype(uri)
        if utype not in ('http', 'https'):
            raise OSError('Unsupported JSONRPC protocol')
        self.__host, self.__handler = splithost(uri)
        if not self.__handler:
            self.__handler = '/RPC2'

        if transport is None:
            transport = SafeTransport() if utype == 'https' else Transport()
        self.__transport = transport

        self.__encoding = encoding
        self.__verbose = verbose
        self.__allow_none = allow_none

    def __request(self, methodname, params):
        """Call a method on the remote server"""
        request = dumps(params, methodname, encoding=self.__encoding, allow_none=self.__allow_none)

        response = self.__transport.request(
            self.__host,
            self.__handler,
            request.encode(self.__encoding),
            self.__encoding,
            verbose=self.__verbose,
        )

        if len(response) == 1:
            response = response[0]

        return response

    def __repr__(self):
        return '<JSONProxy for %s%s>' % (self.__host, self.__handler)

    __str__ = __repr__

    def __getattr__(self, name):
        # dispatch
        return _Method(self.__request, name)

    # note: to call a remote object with an non-standard name, use
    # result getattr(server, "strange-python-name")(args)


if __name__ == '__main__':
    s = ServerProxy('http://localhost:8080/foo/', verbose=1)
    c = s.echo('foo bar')
    print(c)
    d = s.bad('other')
    print(d)
    e = s.echo('foo bar', 'baz')
    print(e)
    f = s.echo(5)
    print(f)
