#@PydevCodeAnalysisIgnore
import re
import compiler
from bike.parsing import visitor
from bike.query.common import getScopeForLine
from bike.parsing.parserutils import generateLogicalLines, \
                makeLineParseable, maskStringsAndRemoveComments
from parser import ParserError
from bike.parsing.fastparserast import Class
from bike.transformer.undo import getUndoStack
from bike.refactor.utils import getTabWidthOfLine, getLineSeperator, \
                reverseCoordsIfWrongWayRound
from bike.transformer.save import queueFileToSave
from bike.parsing.load import getSourceNode
TABSIZE = 4

class coords:
    def __init__(self, line, column):
        self.column = column
        self.line = line
    def __str__(self):
        return "(" + str(self.column) + "," + str(self.line) + ")"

commentRE = re.compile(r"#.*?$")

class ParserException(Exception): pass

def extractMethod(filename, startcoords, endcoords, newname):
    ExtractMethod(getSourceNode(filename),
                  startcoords, endcoords, newname).execute()

class ExtractMethod(object):
    def __init__(self, sourcenode, startcoords, endcoords, newname):
        self.sourcenode = sourcenode

        startcoords, endcoords = \
               reverseCoordsIfWrongWayRound(startcoords, endcoords)

        self.startline = startcoords.line
        self.endline = endcoords.line
        self.startcol = startcoords.column
        self.endcol = endcoords.column

        self.newfn = NewFunction(newname)

        self.getLineSeperator()
        self.adjustStartColumnIfLessThanTabwidth()
        self.adjustEndColumnIfStartsANewLine()
        self.fn = self.getFunctionObject()
        self.getRegionToBuffer()
        #print_ "-"*80
        #print_ self.extractedLines
        #print_ "-"*80
        self.deduceIfIsMethodOrFunction()

    def execute(self):
        self.deduceArguments()
        getUndoStack().addSource(self.sourcenode.filename,
                                 self.sourcenode.getSource())
        srclines = self.sourcenode.getLines()
        newFnInsertPosition = self.fn.getEndLine() - 1
        self.insertNewFunctionIntoSrcLines(srclines, self.newfn,
                                           newFnInsertPosition)
        self.writeCallToNewFunction(srclines)

        src = "".join(srclines)
        queueFileToSave(self.sourcenode.filename, src)        

    def getLineSeperator(self):
        line = self.sourcenode.getLines()[self.startline - 1]
        linesep = getLineSeperator(line)
        self.linesep = linesep        

    def adjustStartColumnIfLessThanTabwidth(self):
        tabwidth = getTabWidthOfLine(self.sourcenode.getLines()[self.startline - 1])
        if self.startcol < tabwidth: self.startcol = tabwidth

    def adjustEndColumnIfStartsANewLine(self):
        if self.endcol == 0:
            self.endline -= 1
            nlSize = len(self.linesep)
            self.endcol = len(self.sourcenode.getLines()[self.endline - 1]) - nlSize


    def getFunctionObject(self):
        return getScopeForLine(self.sourcenode, self.startline)


    def getTabwidthOfParentFunction(self):
        line = self.sourcenode.getLines()[self.fn.getStartLine() - 1]
        match = re.match("\s+", line)
        if match is None:
            return 0
        else:
            return match.end(0)

    # should be in the transformer module
    def insertNewFunctionIntoSrcLines(self, srclines, newfn, insertpos):
        tabwidth = self.getTabwidthOfParentFunction()

        while re.match("\s*" + self.linesep, srclines[insertpos - 1]):
            insertpos -= 1

        srclines.insert(insertpos, self.linesep)
        insertpos += 1

        fndefn = "def " + newfn.name + "("

        if self.isAMethod:
            fndefn += "self"
            if newfn.args != []:
                fndefn += ", " + ", ".join(newfn.args)
        else:
            fndefn += ", ".join(newfn.args)

        fndefn += "):" + self.linesep


        srclines.insert(insertpos, tabwidth * " " + fndefn)
        insertpos += 1

        tabwidth += TABSIZE


        if self.extractedCodeIsAnExpression(srclines):
            assert len(self.extractedLines) == 1

            fnbody = [tabwidth * " " + "return " + self.extractedLines[0]]


        else:
            fnbody = [tabwidth * " " + line for line in self.extractedLines]
            if newfn.retvals != []:
                fnbody.append(tabwidth * " " + "return " + 
                             ", ".join(newfn.retvals) + self.linesep)

        for line in fnbody:
            srclines.insert(insertpos, line)
            insertpos += 1


    def writeCallToNewFunction(self, srclines):
        startline = self.startline
        endline = self.endline
        startcol = self.startcol
        endcol = self.endcol

        fncall = self.constructFunctionCallString(self.newfn.name, self.newfn.args,
                                                  self.newfn.retvals)

        self.replaceCodeWithFunctionCall(srclines, fncall,
                                         startline, endline, startcol, endcol)


    def replaceCodeWithFunctionCall(self, srclines, fncall,
                                    startline, endline, startcol, endcol):
        if startline == endline:  # i.e. extracted code part of existing line
            line = srclines[startline - 1]
            srclines[startline - 1] = self.replaceSectionOfLineWithFunctionCall(line,
                                                         startcol, endcol, fncall)
        else:
            self.replaceLinesWithFunctionCall(srclines, startline, endline, fncall)


    def replaceLinesWithFunctionCall(self, srclines, startline, endline, fncall):
        tabwidth = getTabWidthOfLine(srclines[startline - 1])
        line = tabwidth * " " + fncall + self.linesep
        srclines[startline - 1:endline] = [line]



    def replaceSectionOfLineWithFunctionCall(self, line, startcol, endcol, fncall):
        line = line[:startcol] + fncall + line[endcol:]
        if not line.endswith(self.linesep):
            line += self.linesep
        return line



    def constructFunctionCallString(self, fnname, fnargs, retvals):
        fncall = fnname + "(" + ", ".join(fnargs) + ")"
        if self.isAMethod:
            fncall = "self." + fncall

        if retvals != []:
            fncall = ", ".join(retvals) + " = " + fncall
        return fncall


    def deduceArguments(self):
        lines = self.fn.getLinesNotIncludingThoseBelongingToChildScopes()

        # strip off comments
        lines = [commentRE.sub(self.linesep, line) for line in lines]
        extractedLines = maskStringsAndRemoveComments("".join(self.extractedLines)).splitlines(1)

        linesbefore = lines[:(self.startline - self.fn.getStartLine())]
        linesafter = lines[(self.endline - self.fn.getStartLine()) + 1:]

        # split into logical lines
        linesbefore = [line for line in generateLogicalLines(linesbefore)]        
        extractedLines = [line for line in generateLogicalLines(extractedLines)]
        linesafter = [line for line in generateLogicalLines(linesafter)]

        if self.startline == self.endline:
            # need to include the line code is extracted from
            line = generateLogicalLines(lines[self.startline - self.fn.getStartLine():]).next()
            linesbefore.append(line[:self.startcol] + "dummyFn()" + line[self.endcol:])
        assigns = getAssignments(linesbefore)
        fnargs = getFunctionArgs(linesbefore)
        candidateArgs = assigns + fnargs            
        refs = getVariableReferencesInLines(extractedLines)
        self.newfn.args = [ref for ref in refs if ref in candidateArgs]

        assignsInExtractedBlock = getAssignments(extractedLines)
        usesAfterNewFunctionCall = getVariableReferencesInLines(linesafter)
        usesInPreceedingLoop = getVariableReferencesInLines(
            self.getPreceedingLinesInLoop(linesbefore, line))
        self.newfn.retvals = [ref for ref in usesInPreceedingLoop + usesAfterNewFunctionCall
                                   if ref in assignsInExtractedBlock]

    def getPreceedingLinesInLoop(self, linesbefore, firstLineToExtract):
        if linesbefore == []: return []
        tabwidth = getTabWidthOfLine(firstLineToExtract)
        rootTabwidth = getTabWidthOfLine(linesbefore[0])
        llines = [line for line in generateLogicalLines(linesbefore)]
        startpos = len(llines) - 1
        loopTabwidth = tabwidth
        for idx in range(startpos, 0, -1):
            line = llines[idx]
            if re.match("(\s+)for", line) is not None or \
               re.match("(\s+)while", line) is not None:
                candidateLoopTabwidth = getTabWidthOfLine(line)
                if candidateLoopTabwidth < loopTabwidth:
                    startpos = idx
        return llines[startpos:]

    




    def getRegionToBuffer(self):
        startline = self.startline
        endline = self.endline
        startcol = self.startcol
        endcol = self.endcol


        self.extractedLines = self.sourcenode.getLines()[startline - 1:endline]

        match = re.match("\s*", self.extractedLines[0])
        tabwidth = match.end(0)

        self.extractedLines = [line[startcol:] for line in self.extractedLines]

        # above cropping can take a blank line's newline off.
        # this puts it back
        for idx in range(len(self.extractedLines)):
            if self.extractedLines[idx] == '':
                self.extractedLines[idx] = self.linesep

        if startline == endline:
            # need to crop the end
            # (n.b. if region is multiple lines, then whole lines are taken)
            self.extractedLines[-1] = self.extractedLines[-1][:endcol - startcol]

        if self.extractedLines[-1][-1] != '\n':
            self.extractedLines[-1] += self.linesep

    def extractedCodeIsAnExpression(self, lines):
        if len(self.extractedLines) == 1:
            charsBeforeSelection = lines[self.startline - 1][:self.startcol]
            if re.match("^\s*$", charsBeforeSelection) is not None:
                return 0
            if re.search(":\s*$", charsBeforeSelection) is not None:
                return 0
            return 1
        return 0

    def deduceIfIsMethodOrFunction(self):
        if isinstance(self.fn.getParent(), Class):
            self.isAMethod = 1
        else:
            self.isAMethod = 0


# holds information about the new function
class NewFunction:
    def __init__(self, name):
        self.name = name


# lines = list of lines.
# Have to have strings masked and comments removed
def getAssignments(lines):
    class AssignVisitor:
        def __init__(self):
            self.assigns = []

        def visitAssTuple(self, node):
            for a in node.nodes:
                if a.name not in self.assigns:
                    self.assigns.append(a.name)

        def visitAssName(self, node):
            if node.name not in self.assigns:
                self.assigns.append(node.name)

        def visitAugAssign(self, node):
            if isinstance(node.node, compiler.ast.Name):
                if node.node.name not in self.assigns:
                    self.assigns.append(node.node.name)

    assignfinder = AssignVisitor()
    for line in lines:
        doctoredline = makeLineParseable(line)
        try:
            ast = compiler.parse(doctoredline)
        except ParserError:
            raise ParserException("couldnt parse:" + doctoredline)
        visitor.walk(ast, assignfinder)
    return assignfinder.assigns


# lines = list of lines.
# Have to have strings masked and comments removed
def getFunctionArgs(lines):
    if lines == []: return []

    class FunctionVisitor:
        def __init__(self):
            self.result = []
        def visitFunction(self, node):
            for n in node.argnames:
                if n != "self":
                    self.result.append(n)
    fndef = generateLogicalLines(lines).next()
    doctoredline = makeLineParseable(fndef)
    try:
        ast = compiler.parse(doctoredline)
    except ParserError:
        raise ParserException("couldnt parse:" + doctoredline)
    return visitor.walk(ast, FunctionVisitor()).result



# lines = list of lines. Have to have strings masked and comments removed
def getVariableReferencesInLines(lines):
    class NameVisitor:
        def __init__(self):
            self.result = []
        def visitName(self, node):
            if node.name not in self.result:
                self.result.append(node.name)
    reffinder = NameVisitor()
    for line in lines:
        doctoredline = makeLineParseable(line)
        try:
            ast = compiler.parse(doctoredline)
        except ParserError:
            raise ParserException("couldnt parse:" + doctoredline)
        visitor.walk(ast, reffinder)
    return reffinder.result
