
"""
  Copyright 2011 Jeff Garzik

  AuthServiceProxy has the following improvements over python-jsonrpc's
  ServiceProxy class:

  - HTTP connections persist for the life of the AuthServiceProxy object
    (if server supports HTTP/1.1)
  - sends protocol 'version', per JSON-RPC 1.1
  - sends proper, incrementing 'id'
  - sends Basic HTTP authentication headers
  - parses all JSON numbers that look like floats as Decimal
  - uses standard Python json lib

  Previous copyright, from python-jsonrpc/jsonrpc/proxy.py:

  Copyright (c) 2007 Jan-Klaas Kollhof

  This file is part of jsonrpc.

  jsonrpc is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation; either version 2.1 of the License, or
  (at your option) any later version.

  This software 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 Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with this software; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""

try:
    import http.client as httplib
except ImportError:
    import httplib
import base64
import collections
import decimal
import json
import logging
import os
import socket
try:
    import urllib.parse as urlparse
except ImportError:
    import urlparse

USER_AGENT = "AuthServiceProxy/0.1"

DEFAULT_CONFIG_FILENAME = '~/.reddcoin/reddcoin.conf'
HTTP_TIMEOUT = 30

log = logging.getLogger("ReddcoinRPC")

class JSONRPCException(Exception):
    def __init__(self, rpc_error):
        parent_args = []
        try:
            parent_args.append(rpc_error['message'])
        except:
            pass
        Exception.__init__(self, *parent_args)
        self.error = rpc_error
        self.code = rpc_error['code'] if 'code' in rpc_error else None
        self.message = rpc_error['message'] if 'message' in rpc_error else None

    def __str__(self):
        return '%d: %s' % (self.code, self.message)

    def __repr__(self):
        return '<%s \'%s\'>' % (self.__class__.__name__, self)


def EncodeDecimal(o):
    if isinstance(o, decimal.Decimal):
        return float(round(o, 8))
    raise TypeError(repr(o) + " is not JSON serializable")

class AuthServiceProxy(object):
    __id_count = 0

    # borrowed from pifkoin (https://github.com/dpifke/pifkoin)

    def _parse_config(self, filename=DEFAULT_CONFIG_FILENAME, no_cache=False, **options):
        """
        Returns an OrderedDict with the Reddcoin server configuration.
        Errors are logged; if the configuration file does not exist or could
        not be read, an empty dictionary will be returned; it's up to the
        caller whether or not this is a fatal error.
        :param filename:
            The filename from which the configuration should be read.  Defaults
            to ``~/.reddcoin/reddcoin.conf``.
        :param no_cache:
            If :const:`True`, the configuration will not be memoized and any
            previously memoized configuration will be ignored.
        Any value from the configuration file can also be passed as an
        argument in order to override the value read from disk.
        """

        if filename in getattr(type(self), '_config_cache', {}) and not no_cache:
            config = type(self)._config_cache[filename].copy()
        else:
            # Note: I would have loved to use Python's ConfigParser for this,
            # but it requires .ini-style section headings.
            config = collections.OrderedDict()
            try:
                with open(os.path.expanduser(filename)) as conf:
                    for lineno, line in enumerate(conf):
                        comment = line.find('#')
                        if comment != -1:
                            line = line[:comment]
                        line = line.strip()
                        if not line:
                            continue

                        try:
                            (var, val) = line.split('=')
                        except ValueError:
                            log.warning('Could not parse line %d of %s', lineno, filename)
                            continue

                        var = var.rstrip().lower()

                        val = val.lstrip()
                        if val[0] in ('"', "'") and val[1] in ('"', "'"):
                            val = val[1:-1]

                        config[var] = val

                    conf.close()

            except Exception as e:
                log.error('%s reading %s: %s', type(e).__name__, filename, str(e))

            log.debug('Read %d parameters from %s', len(config), filename)

            if config and not no_cache:
                # At least one parameter was read; memoize the results:
                if not hasattr(type(self), '_config_cache'):
                    type(self)._config_cache = {}
                type(self)._config_cache[filename] = config.copy()

        config.update(options)
        return config

    def __init__(self, service_url=None, service_name=None, connection=None,
                 ssl_context=None, config_filename=DEFAULT_CONFIG_FILENAME, **config_options):

        self.config_filename = config_filename
        self.config_options = config_options
        config = self._parse_config(self.config_filename, **self.config_options)

        if service_url is None:
            self._rpc_host = config.get('rpcserver', '127.0.0.1')
            try:
                socket.gethostbyname(self._rpc_host)
            except socket.error as e:
                raise JSONRPCException('Invalid RPC server %s: %s' % (self._rpc_host, str(e)))

            try:
                self._rpc_port = config.get('rpcport', 45443)
                timeout = int(config.get('rpctimeout', HTTP_TIMEOUT))
            except socket.error as e:
                raise JSONRPCException('Error parsing RPC connection information from %s' % config_filename)

            service_url = "http://%s:%s@%s:%s"%(config['rpcuser'], config['rpcpassword'], self._rpc_host, self._rpc_port)

        self.__service_url = service_url
        self.__service_name = service_name
        self.__url = urlparse.urlparse(service_url)
        if self.__url.port is None:
            port = 45443
        else:
            port = self.__url.port
        (user, passwd) = (self.__url.username, self.__url.password)
        try:
            user = user.encode('utf8')
        except AttributeError:
            pass
        try:
            passwd = passwd.encode('utf8')
        except AttributeError:
            pass
        authpair = user + b':' + passwd
        self.__auth_header = b'Basic ' + base64.b64encode(authpair)

        self.__timeout = timeout = int(config.get('rpctimeout', HTTP_TIMEOUT))

        if connection:
            # Callables re-use the connection of the original proxy
            self.__conn = connection
        elif self.__url.scheme == 'https':
            self.__conn = httplib.HTTPSConnection(self.__url.hostname, port,
                                                  timeout=timeout, context=ssl_context)
        else:
            self.__conn = httplib.HTTPConnection(self.__url.hostname, port,
                                                 timeout=timeout)

    def __getattr__(self, name):
        if name.startswith('__') and name.endswith('__'):
            # Python internal stuff
            raise AttributeError
        if self.__service_name is not None:
            name = "%s.%s" % (self.__service_name, name)
        return AuthServiceProxy(service_url=self.__service_url, service_name=name, timeout=self.__timeout,
                                connection=self.__conn, config_filename=self.config_filename, **self.config_options)

    def __call__(self, *args):
        AuthServiceProxy.__id_count += 1

        log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self.__service_name,
                                 json.dumps(args, default=EncodeDecimal)))
        postdata = json.dumps({'version': '1.1',
                               'method': self.__service_name,
                               'params': args,
                               'id': AuthServiceProxy.__id_count}, default=EncodeDecimal)
        self.__conn.request('POST', self.__url.path, postdata,
                            {'Host': self.__url.hostname,
                             'User-Agent': USER_AGENT,
                             'Authorization': self.__auth_header,
                             'Content-type': 'application/json'})
        self.__conn.sock.settimeout(self.__timeout)

        response = self._get_response()
        if response.get('error') is not None:
            raise JSONRPCException(response['error'])
        elif 'result' not in response:
            raise JSONRPCException({
                'code': -343, 'message': 'missing JSON-RPC result'})
        
        return response['result']

    def batch_(self, rpc_calls):
        """Batch RPC call.
           Pass array of arrays: [ [ "method", params... ], ... ]
           Returns array of results.
        """
        batch_data = []
        for rpc_call in rpc_calls:
            AuthServiceProxy.__id_count += 1
            m = rpc_call.pop(0)
            batch_data.append({"jsonrpc":"2.0", "method":m, "params":rpc_call, "id":AuthServiceProxy.__id_count})

        postdata = json.dumps(batch_data, default=EncodeDecimal)
        log.debug("--> "+postdata)
        self.__conn.request('POST', self.__url.path, postdata,
                            {'Host': self.__url.hostname,
                             'User-Agent': USER_AGENT,
                             'Authorization': self.__auth_header,
                             'Content-type': 'application/json'})
        results = []
        responses = self._get_response()
        if isinstance(responses, (dict,)):
            if ('error' in responses) and (responses['error'] is not None):
                raise JSONRPCException(responses['error'])
            raise JSONRPCException({
                'code': -32700, 'message': 'Parse error'})
        for response in responses:
            if response['error'] is not None:
                raise JSONRPCException(response['error'])
            elif 'result' not in response:
                raise JSONRPCException({
                    'code': -343, 'message': 'missing JSON-RPC result'})
            else:
                results.append(response['result'])
        return results

    def _get_response(self):
        http_response = self.__conn.getresponse()
        if http_response is None:
            raise JSONRPCException({
                'code': -342, 'message': 'missing HTTP response from server'})

        content_type = http_response.getheader('Content-Type')
        if content_type != 'application/json':
            raise JSONRPCException({
                'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)})

        responsedata = http_response.read().decode('utf8')
        response = json.loads(responsedata, parse_float=decimal.Decimal)
        if "error" in response and response["error"] is None:
            log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal)))
        else:
            log.debug("<-- "+responsedata)
        return response
