
""" This module is keeping track about API informations as well as
providing some interface to easily access stored data
"""

import py
import sys
import types
import inspect

from py.__.apigen.tracer.description import FunctionDesc, ClassDesc, \
                                            MethodDesc, Desc

from py.__.apigen.tracer import model

sorted = py.builtin.sorted

def pkg_to_dict(module):
    defs = module.__pkg__.exportdefs
    d = {}
    for key, value in defs.iteritems():
        chain = key.split('.')
        base = module
        for elem in chain:
            base = getattr(base, elem)
        if value[1] == '*':
            d.update(get_star_import_tree(base, key))
        else:
            d[key] = base
    return d

def get_star_import_tree(module, modname):
    """ deal with '*' entries in an initpkg situation """
    ret = {}
    modpath = py.path.local(inspect.getsourcefile(module))
    pkgpath = module.__pkg__.getpath()
    for objname in dir(module):
        if objname.startswith('_'):
            continue # also skip __*__ attributes
        obj = getattr(module, objname)
        if (isinstance(obj, types.ClassType) or
                isinstance(obj, types.ObjectType)):
            try:
                sourcefile_object = py.path.local(
                                        inspect.getsourcefile(obj))
            except TypeError:
                continue
            else:
                if sourcefile_object.strpath != modpath.strpath:
                    # not in this package
                    continue
            dotted_name = '%s.%s' % (modname, objname)
            ret[dotted_name] = obj
    return ret
    
class DocStorage(object):
    """ Class storing info about API
    """
    def __init__(self):
        self.module_name = None

    def consider_call(self, frame, caller_frame, upward_cut_frame=None):
        assert isinstance(frame, py.code.Frame)
        desc = self.find_desc(frame.code, frame.raw.f_locals)
        if desc:
            self.generalize_args(desc, frame)
            desc.consider_call_site(caller_frame, upward_cut_frame)
            desc.consider_start_locals(frame)

    def generalize_args(self, desc, frame):
        args = [arg for key, arg in frame.getargs()]
        #self.call_stack.append((desc, args))
        desc.consider_call([model.guess_type(arg) for arg in args])
    
    def generalize_retval(self, desc, arg):
        desc.consider_return(model.guess_type(arg))

    def consider_return(self, frame, arg):
        assert isinstance(frame, py.code.Frame)
        desc = self.find_desc(frame.code, frame.raw.f_locals)
        if desc:
            self.generalize_retval(desc, arg)
            desc.consider_end_locals(frame)
    
    def consider_exception(self, frame, arg):
        desc = self.find_desc(frame.code, frame.raw.f_locals)
        if desc:
            exc_class, value, _ = arg
            desc.consider_exception(exc_class, value)

    def find_desc(self, code, locals):
        try:
            # argh, very fragile specialcasing
            return self.desc_cache[(code.raw,
                                    locals[code.raw.co_varnames[0]].__class__)]
        except (KeyError, IndexError, AttributeError): # XXX hrmph
            return self.desc_cache.get(code.raw, None)
        #for desc in self.descs.values():
        #    if desc.has_code(frame.code.raw):
        #        return desc
        #return None
    
    def make_cache(self):
        self.desc_cache = {}
        for key, desc in self.descs.iteritems():
            self.desc_cache[desc] = desc
    
    def from_dict(self, _dict, keep_frames=False, module_name=None):
        self.module_name = module_name
        self.descs = {}
        for key, val in _dict.iteritems():
            to_key, to_val = self.make_desc(key, val)
            if to_key:
                self.descs[to_key] = to_val
        self.make_cache()
        # XXX
        return self
    
    # XXX: This function becomes slowly outdated and even might go away at some
    #      point. The question is whether we want to use tracer.magic or not
    #      at all
    def add_desc(self, name, value, **kwargs):
        key = name
        count = 1
        while key in self.descs:
            key = "%s_%d" % (name, count)
            count += 1
        key, desc = self.make_desc(key, value, **kwargs)
        if key:
            self.descs[key] = desc
            self.desc_cache[desc] = desc
            return desc
        else:
            return None
        
    def make_desc(self, key, value, add_desc=True, **kwargs):
        if isinstance(value, types.FunctionType):
            desc = FunctionDesc(key, value, **kwargs)
        elif isinstance(value, (types.ObjectType, types.ClassType)):
            desc = ClassDesc(key, value, **kwargs)
            # XXX: This is the special case when we do not have __init__
            #      in dir(value) for uknown reason. Need to investigate it
            for name in dir(value) + ['__init__']:
                field = getattr(value, name, None)
                if isinstance(field, types.MethodType) and \
                    isinstance(field.im_func, types.FunctionType):
                    real_name = key + '.' + name
                    md = MethodDesc(real_name, field)
                    if add_desc: # XXX hack
                        self.descs[real_name] = md
                    desc.add_method_desc(name, md)
                # Some other fields as well?
        elif isinstance(value, types.MethodType):
            desc = MethodDesc(key, value, **kwargs)
        else:
            desc = Desc(value)
        return (key, desc) # How to do it better? I want a desc to be a key
            # value, but I cannot get full object if I do a lookup

    def from_pkg(self, module, keep_frames=False):
        self.module = module
        self.from_dict(pkg_to_dict(module), keep_frames, module.__name__)
        # XXX
        return self

    def from_module(self, func):
        raise NotImplementedError("From module")

class AbstractDocStorageAccessor(object):
    def __init__(self):
        raise NotImplementedError("Purely virtual object")
    
    def get_function_names(self):
        """ Returning names of all functions
        """
    
    def get_class_names(self):
        """ Returning names of all classess
        """

    def get_doc(self, name):
        """ Returning __doc__ of a function
        """

    def get_function_definition(self, name):
        """ Returns definition of a function (source)
        """
    
    def get_function_signature(self, name):
        """ Returns types of a function
        """

    def get_function_callpoints(self, name):
        """ Returns list of all callpoints
        """
    
    def get_module_name(self):
        pass
    
    def get_class_methods(self, name):
        """ Returns all methods of a class
        """
    
    #def get_object_info(self, key):
    #    
    
    def get_module_info(self):
        """ Returns module information
        """

class DocStorageAccessor(AbstractDocStorageAccessor):
    """ Set of helper functions to access DocStorage, separated in different
    class to keep abstraction
    """
    def __init__(self, ds):
        self.ds = ds

    def _get_names(self, filter):
        return [i for i, desc in self.ds.descs.iteritems() if filter(i, desc)]

    def get_function_names(self):
        return sorted(self._get_names(lambda i, desc: type(desc) is
                                                      FunctionDesc))
    
    def get_class_names(self):
        return sorted(self._get_names(lambda i, desc: isinstance(desc,
                                                                 ClassDesc)))
    
    #def get_function(self, name):
    #    return self.ds.descs[name].pyobj
    
    def get_doc(self, name):
        return self.ds.descs[name].pyobj.__doc__ or "*Not documented*"
    
    def get_function_definition(self, name):
        desc = self.ds.descs[name]
        assert isinstance(desc, FunctionDesc)
        code = py.code.Code(desc.code)
        return code.fullsource[code.firstlineno]
    
    def get_function_signature(self, name):
        desc = self.ds.descs[name]
        # we return pairs of (name, type) here
        names = desc.pyobj.func_code.co_varnames[
                    :desc.pyobj.func_code.co_argcount]
        types = desc.inputcells
        return zip(names, types), desc.retval
    
    def get_function_source(self, name):
        desc = self.ds.descs[name]
        try:
            return str(py.code.Source(desc.pyobj))
        except IOError:
            return "Cannot get source"

    def get_function_callpoints(self, name):
        # return list of tuple (filename, fileline, frame)
        return self.ds.descs[name].get_call_sites()

    def get_function_local_changes(self, name):
        return self.ds.descs[name].get_local_changes()

    def get_function_exceptions(self, name):
        return sorted([i.__name__ for i in self.ds.descs[name].exceptions.keys()])

    def get_module_name(self):
        if self.ds.module_name is not None:
            return self.ds.module_name
        elif hasattr(self.ds, 'module'):
            return self.ds.module.__name__
        return "Unknown module"
    
    def get_class_methods(self, name):
        desc = self.ds.descs[name]
        assert isinstance(desc, ClassDesc)
        return sorted(desc.getfields())
        
    def get_module_info(self):
        module = getattr(self.ds, 'module', None)
        if module is None:
            return "Lack of module info"
        try:
            retval = module.__doc__ or "*undocumented*"
            retval = module.__pkg__.description
            retval = module.__pkg__.long_description
        except AttributeError:
            pass
        return retval

    def get_type_desc(self, _type):
        # XXX We provide only classes here
        if not isinstance(_type, model.SomeClass):
            return None
        # XXX we might want to cache it at some point
        for key, desc in self.ds.descs.iteritems():
            if desc.pyobj == _type.cls:
                return key, 'class', desc.is_degenerated
        return None

    def get_method_origin(self, name):
        method = self.ds.descs[name].pyobj
        cls = method.im_class
        if not cls.__bases__:
            return self.desc_from_pyobj(cls, cls.__name__)
        curr = cls
        while curr:
            for base in curr.__bases__:
                basefunc = getattr(base, method.im_func.func_name, None)
                if (basefunc is not None and hasattr(basefunc, 'im_func') and
                        hasattr(basefunc.im_func, 'func_code') and
                        basefunc.im_func.func_code is
                            method.im_func.func_code):
                    curr = base
                    break
            else:
                break
        return self.desc_from_pyobj(curr, curr.__name__)

    def get_possible_base_classes(self, name):
        cls = self.ds.descs[name].pyobj
        if not hasattr(cls, '__bases__'):
            return []
        retval = []
        for base in cls.__bases__:
            desc = self.desc_from_pyobj(base, base.__name__)
            if desc is not None:
                retval.append(desc)
        return retval

    def desc_from_pyobj(self, pyobj, name):
        for desc in self.ds.descs.values():
            if isinstance(desc, ClassDesc) and desc.pyobj is pyobj:
                return desc
        # otherwise create empty desc
        key, desc = self.ds.make_desc(name, pyobj, False)
        #self.ds.descs[key] = desc
        desc.is_degenerated = True
        # and make sure we'll not try to link to it directly
        return desc

    def get_obj(self, name):
        return self.ds.descs[name].pyobj

