from __future__ import generators
from bike.globals import *
from bike.parsing.fastparserast import Module, Class, Function, getRoot, Instance
from bike.query.common import Match, MatchFinder,\
     getScopeForLine, indexToCoordinates, \
     translateSourceCoordsIntoASTNode, scanScopeForMatches,\
     globalScanForMatches, isAMethod, convertNodeToMatchObject
from compiler.ast import AssName,Name,Getattr,AssAttr
import compiler
from findDefinition import findDefinitionFromASTNode
from bike.query.getTypeOf import getTypeOfExpr, UnfoundType
from bike.query.relationships import getRootClassesOfHierarchy
from bike import log
from bike.parsing.load import getSourceNode


class CouldntFindDefinitionException(Exception):
    pass

def findReferencesIncludingDefn(filename,lineno,col):
    return findReferences(filename,lineno,col,1)


def findReferences(filename,lineno,col,includeDefn=0):
    sourcenode = getSourceNode(filename)
    node = translateSourceCoordsIntoASTNode(filename,lineno,col)
    assert node is not None
    scope,defnmatch = getDefinitionAndScope(sourcenode,lineno,node)

    try:
        for match in findReferencesIncludingDefn_impl(sourcenode,node,
                                                      scope,defnmatch):
            if not includeDefn and match == defnmatch: 
                continue        # don't return definition
            else:
                yield match
    except CouldntFindDefinitionException:
        raise CouldntFindDefinitionException("Could not find definition. Please locate manually (maybe using find definition) and find references from that")

def findReferencesIncludingDefn_impl(sourcenode,node,scope,defnmatch):
    if isinstance(node,Name) or isinstance(node,AssName):
        return generateRefsToName(node.name,scope,sourcenode,defnmatch)
    elif isinstance(node,Getattr) or isinstance(node,AssAttr):
        exprtype = getTypeOfExpr(scope,node.expr)
        if exprtype is None or isinstance(exprtype,UnfoundType):
            raise CouldntFindDefinitionException()

        if isinstance(exprtype,Instance):
            exprtype = exprtype.getType()
            return generateRefsToAttribute(exprtype,node.attrname)
        
        else:
            targetname = node.attrname
            return globalScanForMatches(sourcenode.filename,
                                        NameRefFinder(targetname, defnmatch),
                                        targetname, )
        if match is not None:
            return match
    elif isinstance(node,compiler.ast.Function) or \
                             isinstance(node,compiler.ast.Class):
        return handleClassOrFunctionRefs(scope, node, defnmatch)
    else:
        assert 0,"Seed to references must be Name,Getattr,Function or Class"

def handleClassOrFunctionRefs(scope, node, defnmatch):
    if isAMethod(scope,node):        
        for ref in generateRefsToAttribute(scope,node.name):
            yield ref
    else:
        #yield convertNodeToMatchObject(node,100)
        yield defnmatch
        for ref in generateRefsToName(node.name,scope,
                                    scope.module.getSourceNode(),
                                    defnmatch):
            yield ref

def getDefinitionAndScope(sourcenode,lineno,node):
    scope = getScopeForLine(sourcenode,lineno)
    if scope.getStartLine() == lineno and \
           scope.matchesCompilerNode(node):  # scope is the node
        return scope.getParent(), convertNodeToMatchObject(scope,100)
    defnmatch = findDefinitionFromASTNode(scope,node)
    if defnmatch is None:
        raise CouldntFindDefinitionException()
    scope = getScopeForLine(sourcenode,defnmatch.lineno)
    return scope,defnmatch

def generateRefsToName(name,scope,sourcenode,defnmatch):
    assert scope is not None
    if isinstance(scope,Function):
        # search can be limited to scope
        return scanScopeForMatches(sourcenode,scope,
                                   NameRefFinder(name,defnmatch),
                                   name)
    else:        
        return globalScanForMatches(sourcenode.filename,
                                    NameRefFinder(name,defnmatch),
                                    name)


class NameRefFinder(MatchFinder):
    def __init__(self, targetstr,targetMatch):
        self.targetstr = targetstr
        self.targetMatch = targetMatch
        
    def visitName(self, node):
        if node.name == self.targetstr:
            potentualMatch = findDefinitionFromASTNode(self.scope, node)
            if  potentualMatch is not None and \
                   potentualMatch == self.targetMatch:
                self.appendMatch(node.name)
        self.popWordsUpTo(node.name)

    visitAssName = visitName

    def visitKeyword(self,node):
        self.popWordsUpTo(node.name)  # keyword arg renaming not supported yet
        self.visit(node.expr)        

    def visitFunction(self, node):
        self.popWordsUpTo(node.name)
        for arg, default in self.zipArgs(node.argnames, node.defaults):
            potentualMatch = findDefinitionFromASTNode(self.scope,Name(arg))
            if  potentualMatch is not None and \
                   potentualMatch == self.targetMatch:
                self.appendMatch(arg)
            self.popWordsUpTo(arg)
            if default is not None:
                self.handleDefaultArg(default)
        self.visit(node.code)

    def handleDefaultArg(self, node):
        try:
            if node.name == self.targetstr:
                potentualMatch = findDefinitionFromASTNode(self.scope.getParent(),
                                                           node)
                if  potentualMatch is not None and \
                       potentualMatch == self.targetMatch:
                    self.appendMatch(node.name)
            self.popWordsUpTo(node.name)
        except:
            pass


    def visitFrom(self, node):
        for elem in node.modname.split("."):
            self.popWordsUpTo(elem)
            
        for name, alias in node.names:
            if name == self.targetstr:
                if alias is not None:
                    pretendNode = Name(alias)
                else:
                    pretendNode = Name(name)
                if findDefinitionFromASTNode(self.scope, pretendNode) \
                                                    == self.targetMatch:
                    self.appendMatch(name)
            self.popWordsUpTo(name)
            if alias is not None:
                self.popWordsUpTo(alias)


    def visitGetattr(self, node):
        for c in node.getChildNodes():
            self.visit(c)
        if node.attrname == self.targetstr:
            defn = findDefinitionFromASTNode(self.scope, node)
            if defn is not None and defn == self.targetMatch:
                self.appendMatch(node.attrname)
        self.popWordsUpTo(node.attrname)


    def visitImport(self, node):
        for name, alias in node.names:
            if name.split(".")[-1] == self.targetstr:
                getattr = self.createGetattr(name)
                if findDefinitionFromASTNode(self.scope, getattr) == self.targetMatch:
                    self.appendMatch(self.targetstr)
            for nameelem in name.split("."):
                self.popWordsUpTo(nameelem)
            if alias is not None:
                self.popWordsUpTo(alias)

    
    def createGetattr(self,fqn):
        node = Name(fqn[0])
        for name in fqn.split(".")[1:]:
            node = Getattr(node,name)
        return node
                           
def generateRefsToAttribute(classobj,attrname):
    rootClasses = getRootClassesOfHierarchy(classobj)
    attrRefFinder = AttrbuteRefFinder(rootClasses,attrname)
    for ref in globalScanForMatches(classobj.filename, attrRefFinder, attrname):
        yield ref
    print >>log.progress,"Done"
    

class AttrbuteRefFinder(MatchFinder):
    def __init__(self,rootClasses,targetAttribute):
        self.rootClasses = rootClasses
        self.targetAttributeName = targetAttribute

    
    def visitGetattr(self, node):
        for c in node.getChildNodes():
            self.visit(c)

        if node.attrname == self.targetAttributeName:
            exprtype = getTypeOfExpr(self.scope,node.expr)            

            if isinstance(exprtype,Instance) and \
                 self._isAClassInTheSameHierarchy(exprtype.getType()):
                self.appendMatch(self.targetAttributeName)
            elif isinstance(exprtype,UnfoundType) or \
                 exprtype is None:   # couldn't find type, so not sure
                self.appendMatch(self.targetAttributeName,50)
            else:
                pass # definately not a match
        self.popWordsUpTo(node.attrname)

    visitAssAttr = visitGetattr

    def visitFunction(self,node):  # visit methods
        if node.name == self.targetAttributeName:
            parentScope = self.scope.getParent()
            #print parentScope
            #print self.targetClasses
            if isinstance(parentScope,Class) and \
                   self._isAClassInTheSameHierarchy(parentScope):
                self.appendMatch(node.name)

        for c in node.getChildNodes():
            self.visit(c)
        
    def _isAClassInTheSameHierarchy(self,classobj):
        #return classobj in self.targetClasses
        targetRootClasses = getRootClassesOfHierarchy(classobj)
        for rootclass in self.rootClasses:
            if rootclass in targetRootClasses:
                return True
        return False
