# getTypeOf(scope,fqn) and getTypeOfExpr(scope,ast)

from bike.parsing.fastparserast import Class, Function, Module, Root, getRoot, Package, Instance, getModule
from bike.parsing.parserutils import generateLogicalLines, makeLineParseable,splitLogicalLines, makeLineParseable
from bike.parsing import visitor
from bike import log
from bike.parsing.newstuff import getModuleOrPackageUsingFQN
from bike.parsing.pathutils import getPackageBaseDirectory
from bike.parsing.load import Cache
import os
import re
import compiler

# used if an assignment exists, but cant find type
# e.g. a = SomeFunctionNotLoaded()
# (as opposed to 'None' if cant find an assignment)
class UnfoundType: pass


getTypeOfStack = []

# name is the fqn of the reference, scope is the scope ast object from
# which the question is being asked.
# returns an fastparser-ast object representing the type
# or None if type not found
def getTypeOf(scope, fqn):
    if isinstance(scope, Root):
        assert False, "Can't use getTypeOf to resolve from Root. Use getModuleOrPackageUsingFQN instead"

    #print "getTypeOf:"+fqn+" -- "+str(scope)
    #print 
    #print str(getTypeOfStack)
    #print 

    # wraps the getTypeOf_impl method, caching results and protecting from
    # stack overflow 
    if (fqn,scope) in getTypeOfStack:   # loop protection
        return None
    try:
        getTypeOfStack.append((fqn,scope))
        try:
            hashcode = str(scope)+fqn  # this is crap!
            type = Cache.instance.typecache[hashcode]
        except KeyError:
            type = getTypeOf_impl(scope, fqn)
            Cache.instance.typecache[hashcode] = type
        return type
    finally:
        del getTypeOfStack[-1]
        


def getTypeOf_impl(scope, fqn):
    if fqn == "None":
        return None

    if "."in fqn:
        rcdr = ".".join(fqn.split(".")[:-1])
        rcar = fqn.split(".")[-1]
        newscope = getTypeOf(scope,rcdr)
        if newscope is not None:
            return getTypeOf(newscope, rcar)
        else: 
            #print "couldnt find "+rcdr+" in "+str(scope)
            pass

    assert scope is not None
    #assert not ("." in fqn) 
    
    if isinstance(scope,UnfoundType):
        return UnfoundType()
    
    if isinstance(scope, Package):
        #assert 0,scope
        return handlePackageScope(scope, fqn)
    elif isinstance(scope,Instance):
        return handleClassInstanceAttribute(scope, fqn)
    else:
        return handleModuleClassOrFunctionScope(scope,fqn)



def handleModuleClassOrFunctionScope(scope,name):
    if name == "self" and isinstance(scope,Function) and \
           isinstance(scope.getParent(),Class):
        return Instance(scope.getParent())
    
    matches = [c for c in scope.getChildNodes()if c.name == name]
    if matches != []:
        return matches[0]
    
    type = scanScopeSourceForType(scope, name)
    if type != None:
        return type
    
    #print "name = ",name,"scope = ",scope
    type = getImportedType(scope, name)   # try imported types
    #print "type=",type
    if type != None:
        return type
    parentScope = scope.getParent()
    while isinstance(parentScope,Class):
        # don't search class scope, since this is not accessible except
        # through self   (is this true?)
        parentScope = parentScope.getParent()

    if not (isinstance(parentScope,Package) or isinstance(parentScope,Root)):
        return getTypeOf(parentScope, name)


def handleClassInstanceAttribute(instance, attrname):
    theClass = instance.getType()

    # search methods and inner classes
    match = theClass.getChild(attrname)
    if match:
        return match

    #search methods for assignments with self.foo getattrs
    for child in theClass.getChildNodes():
        if not isinstance(child,Function):
            continue
        res = scanScopeAST(child,attrname,
                          SelfAttributeAssignmentVisitor(child,attrname))
        if res is not None:
            return res

def handlePackageScope(package, fqn):
    #print "handlePackageScope",package,fqn
    child = package.getChild(fqn)
    if child:
        return child

    if isinstance(package,Root):
        return getModuleOrPackageUsingFQN(fqn)

    # try searching the fs
    node = getModuleOrPackageUsingFQN(fqn,package.path)
    if node:
        return node
    
    


    # try the package init module
    initmod = package.getChild("__init__")
    if initmod is not None:
        type = getImportedType(initmod, fqn)
        if type:
            return type
    # maybe fqn is absolute
    return getTypeOf(getRoot(), fqn)


wordRE = re.compile("\w+")
def isWordInLine(word, line):
    if line.find(word) != -1:
        words = wordRE.findall(line)
        if word in words:
            return 1
    return 0

def getImportedType(scope, fqn):
    lines = scope.module.getSourceNode().getLines()
    for lineno in scope.getImportLineNumbers():
        logicalline = generateLogicalLines(lines[lineno-1:]).next()
        logicalline = makeLineParseable(logicalline)
        ast = compiler.parse(logicalline)
        match = visitor.walk(ast, ImportVisitor(scope,fqn)).match
        if match:
            return match
        #else loop

class ImportVisitor:
    def __init__(self,scope,fqn):
        self.match = None
        self.targetfqn = fqn
        self.scope = scope

    def visitImport(self, node):
        # if target fqn is an import, then it must be a module or package
        for name, alias in node.names:
            if name == self.targetfqn:
                self.match = resolveImportedModuleOrPackage(self.scope,name)
            elif alias is not None and alias == self.targetfqn:
                self.match = resolveImportedModuleOrPackage(self.scope,name)

    def visitFrom(self, node):
        if node.names[0][0] == '*': # e.g. from foo import *
            if not "."in self.targetfqn:
                module = resolveImportedModuleOrPackage(self.scope,
                                                        node.modname)
                if module:
                    self.match = getTypeOf(module, self.targetfqn)
        else:
            for name, alias in node.names:
                if alias == self.targetfqn or \
                   (alias is None and name == self.targetfqn):
                    scope = resolveImportedModuleOrPackage(self.scope,
                                                            node.modname)
                    if scope is not None:
                        if isinstance(scope,Package):
                            self.match = getModuleOrPackageUsingFQN(name,scope.path)
                        else:  
                            assert isinstance(scope,Module)
                            self.match = getTypeOf(scope, name)




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

    def __str__(self):
        return self.msg

# attempts to evaluate the type of the expression
def getTypeOfExpr(scope, ast):
    if isinstance(ast, compiler.ast.Name):
        return getTypeOf(scope, ast.name)

    elif isinstance(ast, compiler.ast.Getattr) or \
             isinstance(ast, compiler.ast.AssAttr):

        # need to do this in order to match foo.bah.baz as
        # a string in import statements
        fqn = attemptToConvertGetattrToFqn(ast)
        if fqn is not None:
            return getTypeOf(scope,fqn)

        expr = getTypeOfExpr(scope, ast.expr)
        if expr is not None:
            attrnametype = getTypeOf(expr, ast.attrname)
            return attrnametype
        return None

    elif isinstance(ast, compiler.ast.CallFunc):
        node = getTypeOfExpr(scope,ast.node)
        if isinstance(node,Class):
            return Instance(node)
        elif isinstance(node,Function):
            return getReturnTypeOfFunction(node)
    else:
        #raise TypeNotSupportedException, \
        #      "Evaluation of "+str(ast)+" not supported. scope="+str(scope)
        print >> log.warning, "Evaluation of "+str(ast)+" not supported. scope="+str(scope)
        return None


def attemptToConvertGetattrToFqn(ast):
    fqn = ast.attrname
    ast = ast.expr
    while isinstance(ast,compiler.ast.Getattr):
        fqn = ast.attrname + "." + fqn
        ast = ast.expr
    if isinstance(ast,compiler.ast.Name):
        return ast.name + "." + fqn
    else:
        return None


getReturnTypeOfFunction_stack = []
def getReturnTypeOfFunction(function):
    if function in getReturnTypeOfFunction_stack:   # loop protection
        return None
    try:
        getReturnTypeOfFunction_stack.append(function)
        return getReturnTypeOfFunction_impl(function)
    finally:
        del getReturnTypeOfFunction_stack[-1]

def getReturnTypeOfFunction_impl(function):
    return scanScopeAST(function,"return",ReturnTypeVisitor(function))
    

# does parse of scope sourcecode to deduce type
def scanScopeSourceForType(scope, name):
    return scanScopeAST(scope,name,AssignmentVisitor(scope,name))


# scans for lines containing keyword, and then runs the visitor over
# the parsed AST for that line
def scanScopeAST(scope,keyword,astvisitor):
    lines = scope.getLinesNotIncludingThoseBelongingToChildScopes()
    src = ''.join(lines)
    match = None
    #print "scanScopeAST:"+str(scope)
    for line in splitLogicalLines(src):
        if isWordInLine(keyword, line):
            #print "scanning for "+keyword+" in line:"+line[:-1]
            doctoredline = makeLineParseable(line)
            ast = compiler.parse(doctoredline)
            match = visitor.walk(ast,astvisitor).getMatch()
            if match:
                return match
    return match
    

class AssignmentVisitor:
    def __init__(self,scope,targetName):
        self.match=None
        self.scope = scope
        self.targetName = targetName

    def getMatch(self):
        return self.match

    def visitAssign(self,node):
        if isinstance(node.expr,compiler.ast.CallFunc):
            for assnode in node.nodes:
                if isinstance(assnode,compiler.ast.AssName) and \
                   assnode.name == self.targetName:
                    self.match = getTypeOfExpr(self.scope,node.expr)
                    if self.match is None:
                        self.match = UnfoundType()
                    
                    

class SelfAttributeAssignmentVisitor:
    def __init__(self,scope,targetName):
        self.match=None
        self.scope = scope
        self.targetName = targetName

    def getMatch(self):
        return self.match

    def visitAssign(self,node):
        if isinstance(node.expr,compiler.ast.CallFunc):
            for assnode in node.nodes:
                if isinstance(assnode,compiler.ast.AssAttr) and \
                   isinstance(assnode.expr,compiler.ast.Name) and \
                   assnode.expr.name == "self" and \
                   assnode.attrname == self.targetName:
                    self.match = getTypeOfExpr(self.scope,node.expr)
                    #print "here!",self.match.getType().fqn


class ReturnTypeVisitor:
    def __init__(self,fn):
        self.match=None
        self.fn = fn

    def getMatch(self):
        return self.match
    
    def visitReturn(self,node):
        try:
            self.match = getTypeOfExpr(self.fn,node.value)
        except TypeNotSupportedException, ex:
            pass


def resolveImportedModuleOrPackage(scope,fqn):
    # try searching from directory containing scope module
    path = os.path.dirname(scope.module.filename)
    node = getModuleOrPackageUsingFQN(fqn,path)
    if node is not None:
        return node

    # try searching in same package hierarchy
    basedir = getPackageBaseDirectory(scope.module.filename)
    if fqn.split('.')[0] == os.path.split(basedir)[-1]:
        # base package in fqn matches base directory
        restOfFqn = ".".join(fqn.split('.')[1:])
        node = getModuleOrPackageUsingFQN(restOfFqn,basedir)
    if node is not None:
        return node

    # try searching the python path
    node = getModuleOrPackageUsingFQN(fqn)
    if node is not None:
        return node
