from __future__ import generators
from bike.globals import *
from bike.parsing.fastparserast import getRoot, Function, Class, Module, getModule
from bike.parsing.parserutils import generateLogicalLines, makeLineParseable, UnbalancedBracesException, generateLogicalLinesAndLineNumbers
from bike.parsing.newstuff import getSourceNodesContainingRegex
from bike.parsing import visitor
from bike import log
import compiler
from compiler.ast import Getattr, Name
import re

class Match:
    def __repr__(self):
        return ",".join([self.filename, str(self.lineno), str(self.colno),
                         str(self.confidence)])
    def __eq__(self,other):
        if self is None or other is None:
            return False
        return self.filename == other.filename and \
               self.lineno == other.lineno and \
               self.colno == other.colno

def getScopeForLine(sourceNode, lineno):
    scope = None
    childnodes = sourceNode.getFlattenedListOfFastParserASTNodes()
    if childnodes == []:
        return sourceNode.fastparseroot #module node

    scope = sourceNode.fastparseroot

    for node in childnodes:
        if node.linenum > lineno: break
        scope = node

    if scope.getStartLine() != scope.getEndLine(): # is inline
        while scope.getEndLine() <= lineno:
            scope = scope.getParent()
    return scope



# global from the perspective of 'contextFilename'
def globalScanForMatches(contextFilename, matchFinder, targetname):
    for sourcenode in getSourceNodesContainingRegex(targetname, contextFilename):
        print >> log.progress, "Scanning", sourcenode.filename
        searchscope = sourcenode.fastparseroot
        for match in scanScopeForMatches(sourcenode,searchscope,
                                         matchFinder,targetname):
            yield match


def scanScopeForMatches(sourcenode,scope,matchFinder,targetname):
    lineno = scope.getStartLine()
    for line in generateLogicalLines(scope.getMaskedLines()):
        if line.find(targetname) != -1:
            doctoredline = makeLineParseable(line)
            ast = compiler.parse(doctoredline)
            scope = getScopeForLine(sourcenode, lineno)
            matchFinder.reset(line)
            matchFinder.setScope(scope)
            matches = visitor.walk(ast, matchFinder).getMatches()
            for index, confidence in matches:
                match = Match()
                match.filename = sourcenode.filename
                match.sourcenode = sourcenode
                x, y = indexToCoordinates(line, index)
                match.lineno = lineno+y
                match.colno = x
                match.colend = match.colno+len(targetname)
                match.confidence = confidence
                yield match
        lineno+=line.count("\n")
    

def walkLinesContainingStrings(scope,astWalker,targetnames):
    lineno = scope.getStartLine()
    for line in generateLogicalLines(scope.getMaskedLines()):
        if lineContainsOneOf(line,targetnames):
            doctoredline = makeLineParseable(line)
            ast = compiler.parse(doctoredline)
            astWalker.lineno = lineno
            matches = visitor.walk(ast, astWalker)
        lineno+=line.count("\n")


def lineContainsOneOf(line,targetnames):
    for name in targetnames:
        if line.find(name) != -1:
            return True
    return False


# translates an idx in a logical line into physical line coordinates
# returns x and y coords
def indexToCoordinates(src, index):
    y = src[: index].count("\n")
    startOfLineIdx = src.rfind("\n", 0, index)+1
    x = index-startOfLineIdx
    return x, y



# interface for MatchFinder classes
# implement the visit methods
class MatchFinder:
    def setScope(self, scope):
        self.scope = scope

    def reset(self, line):
        self.matches = []
        self.words = re.split("(\w+)", line) # every other one is a non word
        self.positions = []
        i = 0
        for word in self.words:
            self.positions.append(i)
            #if '\n' in word:  # handle newlines
            #    i = len(word[word.index('\n')+1:])
            #else:
            i+=len(word)
        self.index = 0

    def getMatches(self):
        return self.matches

    # need to visit childnodes in same order as they appear
    def visitPrintnl(self,node):
        if node.dest:
            self.visit(node.dest)
        for n in node.nodes:
            self.visit(n)
    
    def visitName(self, node):
        self.popWordsUpTo(node.name)

    def visitClass(self, node):
        self.popWordsUpTo(node.name)
        for base in node.bases:
            self.visit(base)

    def zipArgs(self, argnames, defaults):
        """Takes a list of argument names and (possibly a shorter) list of
        default values and zips them into a list of pairs (argname, default).
        Defaults are aligned so that the last len(defaults) arguments have
        them, and the first len(argnames) - len(defaults) pairs have None as a
        default.
        """
        fixed_args = len(argnames) - len(defaults)
        defaults = [None] * fixed_args + list(defaults)
        return zip(argnames, defaults)

    def visitFunction(self, node):
        self.popWordsUpTo(node.name)
        for arg, default in self.zipArgs(node.argnames, node.defaults):
            self.popWordsUpTo(arg)
            if default is not None:
                self.visit(default)
        self.visit(node.code)

    def visitGetattr(self,node):
        self.visit(node.expr)
        self.popWordsUpTo(node.attrname)

    def visitAssName(self, node):
        self.popWordsUpTo(node.name)

    def visitAssAttr(self, node):
        self.visit(node.expr)
        self.popWordsUpTo(node.attrname)

    def visitImport(self, node):
        for name, alias in node.names:
            for nameelem in name.split("."):
                self.popWordsUpTo(nameelem)
            if alias is not None:
                self.popWordsUpTo(alias)

    def visitFrom(self, node):
        for elem in node.modname.split("."):
            self.popWordsUpTo(elem)
        for name, alias in node.names:
            self.popWordsUpTo(name)
            if alias is not None:
                self.popWordsUpTo(alias)

    def visitLambda(self, node):
        for arg, default in self.zipArgs(node.argnames, node.defaults):
            self.popWordsUpTo(arg)
            if default is not None:
                self.visit(default)
        self.visit(node.code)

    def visitGlobal(self, node):
        for name in node.names:
            self.popWordsUpTo(name)

    def popWordsUpTo(self, word):
        if word == "*":
            return        # won't be able to find this
        posInWords = self.words.index(word)
        idx = self.positions[posInWords]
        self.words = self.words[posInWords+1:]
        self.positions = self.positions[posInWords+1:]

    def appendMatch(self,name,confidence=100):
        idx = self.getNextIndexOfWord(name)
        self.matches.append((idx, confidence))

    def getNextIndexOfWord(self,name):
        return self.positions[self.words.index(name)]

class CouldNotLocateNodeException(Exception): pass

def translateSourceCoordsIntoASTNode(filename,lineno,col):
    module = getModule(filename)
    maskedlines = module.getMaskedModuleLines()
    lline,backtrackchars = getLogicalLine(module, lineno)
    doctoredline = makeLineParseable(lline)
    ast = compiler.parse(doctoredline)
    idx = backtrackchars+col
    nodefinder = ASTNodeFinder(lline,idx)
    node = compiler.walk(ast, nodefinder).node
    if node is None:
        raise CouldNotLocateNodeException("Could not translate editor coordinates into source node")
    return node

def getLogicalLine(module, lineno):
    # we know that the scope is the start of a logical line, so
    # we search from there
    scope = getScopeForLine(module.getSourceNode(), lineno)
    linegenerator = \
            module.generateLinesWithLineNumbers(scope.getStartLine())
    for lline,llinenum in \
            generateLogicalLinesAndLineNumbers(linegenerator):
        if llinenum > lineno:
            break
        prevline = lline
        prevlinenum = llinenum

    backtrackchars = 0
    for i in range(prevlinenum,lineno):
        backtrackchars += len(module.getSourceNode().getLines()[i-1])
    return prevline, backtrackchars



class ASTNodeFinder(MatchFinder):
    # line is a masked line of text
    # lineno and col are coords
    def __init__(self,line,col):
        self.line = line
        self.col = col
        self.reset(line)
        self.node = None

    def visitName(self,node):
        if self.checkIfNameMatchesColumn(node.name):
            self.node = node
        self.popWordsUpTo(node.name)

    def visitGetattr(self,node):
        self.visit(node.expr)
        if self.checkIfNameMatchesColumn(node.attrname):
            self.node = node
        self.popWordsUpTo(node.attrname)

    def visitKeyword(self,node):
        self.popWordsUpTo(node.name)
        self.visit(node.expr)

    def visitFunction(self, node):
        if self.checkIfNameMatchesColumn(node.name):
            self.node = node
        self.popWordsUpTo(node.name)

        for arg, default in self.zipArgs(node.argnames, node.defaults):
            if self.checkIfNameMatchesColumn(arg):
                self.node = Name(arg)
            self.popWordsUpTo(arg)
            if default is not None:
                self.visit(default)
        self.visit(node.code)


    visitAssName = visitName
    visitAssAttr = visitGetattr

    def visitClass(self, node):
        if self.checkIfNameMatchesColumn(node.name):
            self.node = node
        self.popWordsUpTo(node.name)
        for base in node.bases:
            self.visit(base)


    def checkIfNameMatchesColumn(self,name):
        idx = self.getNextIndexOfWord(name)
        #print "name",name,"idx",idx,"self.col",self.col
        if idx <= self.col and idx+len(name) > self.col:
            return 1
        return 0

    def visitFrom(self, node):
        for elem in node.modname.split("."):
            self.popWordsUpTo(elem)
        for name, alias in node.names:
            if self.checkIfNameMatchesColumn(name):
                self.node = self._manufactureASTNodeFromFQN(name)
                return
            self.popWordsUpTo(name)
            if alias is not None:
                self.popWordsUpTo(alias)

    # gets round the fact that imports etc dont contain nested getattr
    # nodes for fqns (e.g. import a.b.bah) by converting the fqn
    # string into a getattr instance
    def _manufactureASTNodeFromFQN(self,fqn):
        if "." in fqn:
            assert 0, "getattr not supported yet"
        else:
            return Name(fqn)

def isAMethod(scope,node):
    return isinstance(node,compiler.ast.Function) and \
           isinstance(scope,Class)

def scopeIsAMethod(scope):
    return isinstance(scope,Function) and isinstance(scope.getParent(),Class)


def convertNodeToMatchObject(node,confidence=100):
    m = Match()
    m.sourcenode = node.module.getSourceNode()
    m.filename = node.filename
    if isinstance(node,Module):
        m.lineno = 1
        m.colno = 0
    elif isinstance(node,Class) or isinstance(node,Function):
        m.lineno = node.getStartLine()
        m.colno = node.getColumnOfName()
    m.confidence = confidence
    return m
