#! /usr/bin/env python
#
# rocket - implementing with a quickness
#
# This module's design is influenced by the work of 
# Samuel Cormier-Iijima and team on the pyfacebook module.
# http://github.com/sciyoshi/pyfacebook/
#
# Props.

import sys
import time
import struct
import urllib
import urllib2
import httplib
import binascii
import urlparse
import mimetypes
import logging

from collections import defaultdict

import proxies
from auth import sign_args
import http_handling

try:
    from hashlib import md5
except ImportError:
    from md5 import md5



### Would like a hand making this appengine safe
try:
    from json import dumps as json_encode
    from json import loads as json_decode
except ImportError:
    try:
        from simplejson import dumps as json_encode
        from simplejson import loads as json_decode
    except ImportError:
        quit('Rocket requires json support')


# create json object for referencing complex structures in function_list
class json(object): pass


#########################################
# Rocket Errors #########################
#########################################

class RocketException(Exception):
    """Exception class for errors received from Rocket."""

    def __init__(self, msg):
        self.msg = msg

    def __str__(self):
        return '%s' % (self.msg)
    

#########################################
# Logging context #######################
#########################################

def logging_context(log_stream=sys.stdout, log_level=logging.INFO):
    logger = logging.getLogger("logger_rocket")
    logger.setLevel(log_level)
    
    # set stream from user, user sys.stdout as rocket default
    sh = logging.StreamHandler( log_stream )
    # set the log level requested by user, default is logging.info
    sh.setLevel(log_level)
    formatter = logging.Formatter("%(asctime)s %(process)d %(filename)s" \
                                "%(lineno)d %(levelname)s #rocket| %(message)s") 
    sh.setFormatter(formatter)
    logger.addHandler(sh)
    
    return logger    

#########################################
# Events ################################
#########################################

class Event(object):

    def __init__(self, query_url, secure, ns_fun, method, data):
        self.query_url = query_url
        self.secure = secure
        self.data = data
        self.ns_fun = ns_fun
        self.method = method
        self.succeed = None

    def success(self, response):
        self.succeed = True
        self.response = response

    def fail(self, exception):
        self.succeed = False
        self.result = exception 

#########################################
# The Rocket ############################
#########################################

class Rocket(object):
    """Provides access to most features necessary for an API
    implementation. 

    All configuration options are keywords, since every API is different.
    """

    def __init__(self, function_list,
                 client='rocket', gen_doc_str=None,
                 api_key=None, api_secret_key=None,
                 api_url=None, api_url_secure=None,
                 basic_auth_pair=None, basic_auth_realm=None,
                 web_proxy=None, format=http_handling.DEFAULT_RESPONSE_FORMAT,
                 gen_namespace_pair=proxies.gen_ns_pair_default, 
                 log_stream=sys.stdout, log_level=logging.INFO,
                 *args, **kwargs):
        """Initializes a new Rocket which provides wrappers for the
        API implementation.

        The namespace map saves a namespace as it originally appeared in
        FUNCTIONS
        """
        self.function_list = function_list
        self.api_key = api_key
        self.api_secret_key = api_secret_key
        self.format = format
        self.web_proxy = web_proxy
        self.api_url = api_url
        self.api_url_secure = api_url_secure
        self.basic_auth_pair = basic_auth_pair
        self.basic_auth_realm = basic_auth_realm
        self.gen_namespace_pair = gen_namespace_pair
        self._log_level=log_level
        self._log_stream=log_stream
        self._callbacks = defaultdict(lambda : defaultdict(set))
        
        logger = logging_context(log_stream=log_stream, log_level=log_level)
        logger.debug("Create rocket: url: %s client %s" % (api_url,client))

        rocket_proxies = proxies.generate_proxies(function_list,
                                                  gen_doc_str,
                                                  logger=logger,
                                                  gen_namespace_pair=self.gen_namespace_pair)

        self.namespace_map = {}
        for namespace in self.function_list:
            (ns_name, ns_title) = self.gen_namespace_pair(namespace)
            proxy_class = rocket_proxies['%sProxy' % ns_title]
            # TODO: setattr?
            self.__dict__[ns_name] = proxy_class(self,
                                                 '%s.%s' % (client, ns_name))
            self.namespace_map[ns_name] = namespace
            
            
    def _expand_arguments(self, args):
        """Expands arguments from native type to web friendly type
        """
        logger = logging_context(log_stream=self._log_stream, 
                                 log_level=self._log_level)
        logger.debug("rocket _expand_arguments %s" % args )
        
        for arg in args.items():
            if type(arg[1]) == list:
                args[arg[0]] = ','.join(str(a) for a in arg[1])
            elif type(arg[1]) == unicode:
                args[arg[0]] = arg[1].encode("UTF-8")
            elif type(arg[1]) == bool:
                args[arg[0]] = str(arg[1]).lower()
        return args


    def _parse_response(self, response, method):
        """Parses the response according to the given (optional) format,
        which should be 'json'.
        """
        logger = logging_context(log_stream=self._log_stream, 
                                 log_level=self._log_level)
        logger.debug("rocket _parse_response, response=%s and method=%s"
                     % (response, method) )
        
        if self.format == http_handling.RESPONSE_JSON:
            json = response[2]
            result = json_decode(json)
            self.check_error(result)
        else:
            logger.critical( response )
            raise RuntimeError('Invalid format specified.')
        return result


    def __call__(self, function_key=None, args=None, secure=False):
        """Mediator for calls to dynamic methods. Constructs environment
        based on arguments and calls appropriate Proxy object for
        function behavior.
        """
        # for Django templates, if this object is called without any arguments
        # return the object itself
        if function_key is None:
            return self

        # Entries in function_list are app_name.[namespace]function.method
        key_parts = function_key.split('.')
        num_parts = len(key_parts)
        if num_parts < 3:
            raise rocket.RocketException('Incorrect function_list definition')

        # Break the function name into three parts:
        #     object name, namespace, http connection method
        obj_name = key_parts[0]
        namespace = '.'.join(key_parts[1:-1])
        method = key_parts[-1]
        (ns_fun, ns_title) = self.gen_namespace_pair(namespace)

        args = self.build_query_args(method, args=args)

        api_url = self.api_url
        if secure:
            api_url = self.api_url_secure
        query_url = self.gen_query_url(api_url, ns_fun, 
                                       format=self.format,
                                       method=method,
                                       get_args=args)
        event = Event(query_url, secure, ns_fun, method, args)

        try:
            if self.web_proxy:
                web_proxy_handler = urllib2.ProxyHandler(self.web_proxy)
                opener = urllib2.build_opener(web_proxy_handler)
                response = opener.open(query_url).read()
            else:
                response = http_handling.urlread(query_url, data=args,
                                                 method=method.upper(),
                                                 basic_auth_pair=self.basic_auth_pair,
                                                 basic_auth_realm=self.basic_auth_realm,
                                                 logger=logging_context())

                if response:
                    response = self._parse_response(response, method)
                else:
                    response = None
        except Exception, ex_inst:
            event.fail(ex_inst)
            raise
        else:
            event.success(response)
        finally:
            self.fire_callbacks(event)

        return response
    
    ########################################
    # Callbacks ############################
    ########################################

    def add_callback(self, method, ns_fun, callback):
        self._callbacks[method][ns_fun].add(callback)

    def add_callbacks(self, methods, ns_funs, callback):
        for method in methods:
            for ns_fun in ns_funs:
                self.add_callback(method, ns_fun, callback)

    def remove_callback(self, method, ns_fun, callback):
        self._callbacks[method][ns_fun].remove(callback)

    def remove_callbacks(self, methods, ns_funs, callback):
        for method in methods:
            for ns_fun in ns_funs:
                self.remove_callback(method, ns_fun, callback)

    def fire_callbacks(self, event): 
        
        callbacks_collections = (self._callbacks[event.method][event.ns_fun],                                
                                 self._callbacks[event.method][None],
                                 self._callbacks[None][None],
                                 )

        for callbacks in callbacks_collections:
            for callback in callbacks:
                callback(event)
            
            if callbacks: # stop fire a callback on a first not empty stack
                break

    def check_error(self, response):
        """Some API's transmit errors over successful HTTP connections, eg. 200.

        Implement this function to handle parsing a response for errors.
        """
        pass

    
    def gen_query_url(self, url, function, format=None, method=None, 
                      get_args=None):
        """Generates URL for request according to structure of IDL.

        Implementation formats worth considering:
            url/function.format # self.format
            url/function
        """
        return '%s/%s' % (url, function)

    
    def build_query_args(self, method, args=None,
                         signing_alg=None):
        """Adds to args parameters that are necessary for every call to
        the API.
        """
        if args is None:
            raise RuntimeError('Arguments required for call to '
                               'build_query_args')

        args = self._expand_arguments(args)
        
        if self.format:
            args['format'] = self.format
        
        if self.api_key:
            args['api_key'] = self.api_key

        if self.api_secret_key:
            if signing_alg == None:
                signing_alg = sign_args            
            args['sig'] = signing_alg(args, self.api_secret_key)

        return args


