import ast
import os.path
import sys
import codecs
import datetime

def _get_last_line_number(node):
    if hasattr(node,'lineno'):
        max_line_number = node.lineno
    else:
        return -1
    for child_node in ast.iter_child_nodes(node):
        max_line_number = max(_get_last_line_number(child_node),max_line_number)
    return max_line_number

def sanitize(value):
    if not value:
        return ''
    try:
        return unicode(value)
    except:
        return "(invalid string value)"

class AnalysisNodeVisitor(ast.NodeVisitor):
    
    def __init__(self,parentNode = None):
        self._modules = []
        self._classes = []
        self._functions = []
        self._complexity_watchers = []
        self._variables = []
        self._imports = []
        self._parentNode = parentNode
        self._level = 0

    def _visit_node_and_add_to_complexity(self, node):
        for complexity_watcher in self._complexity_watchers:
            complexity_watcher['cyclomatic_complexity'] += 1
    
        ast.NodeVisitor.generic_visit(self,node)

    def visit_ListComp(self,node):
        ast.NodeVisitor.generic_visit(self,node)

    def visit_GeneratorExp(self,node):
        ast.NodeVisitor.generic_visit(self,node)

    visit_For = visit_If   \
            = visit_IfExp  \
            = visit_While = visit_With = _visit_node_and_add_to_complexity

    @property
    def imports(self):
        return self._imports
    
    @property
    def functions(self):
        return self._functions
    
    @property
    def variables(self):
        return self._variables
    
    @property
    def classes(self):
        return self._classes

    def visit_Import(self,node):
        importNode = Node(attributes = {'line_number':node.lineno,'type':'import','names':map(lambda x:x.name,node.names)},parent = self._parentNode)
        self._imports.append(importNode)
        ast.NodeVisitor.generic_visit(self, node)

    def visit_ImportFrom(self,node):
        importNode = Node(attributes = {'line_number':node.lineno,'type':'from_import','module':node.module,'names':map(lambda x:x.name,node.names)},parent = self._parentNode)
        self._imports.append(importNode)
        ast.NodeVisitor.generic_visit(self, node)

    def visit_Assign(self,node):
        for target in node.targets:
            self._add_target_to_variables(target)
        ast.NodeVisitor.generic_visit(self, node)

    def visit_AssignAug(self,node):
        self._add_target_to_variables(node.target)
        ast.NodeVisitor.generic_visit(self, node)

    def _add_target_to_variables(self,target):
        if hasattr(target,'value'):
            self._add_target_to_variables(target.value)
        elif hasattr(target,'id'):
            if not target.id in self._variables and not target.id == "self":
                variableNode = Node(attributes = {'line_number':target.lineno,'type':'variable','name':target.id},parent = self._parentNode)
                self._variables.append(variableNode)

    def visit_FunctionDef(self,node):
        functionNode = Node(attributes = {'cyclomatic_complexity' : 1,'type':'function','name':node.name,'start_line':node.lineno,'end_line':_get_last_line_number(node),'docstring':sanitize(ast.get_docstring(node,clean = True))},parent = self._parentNode)
        self._functions.append(functionNode)
        oldParent = self._parentNode
        self._parentNode = functionNode

        self._complexity_watchers.append(functionNode)
        ast.NodeVisitor.generic_visit(self, node)
        self._complexity_watchers.remove(functionNode)
        
        self._parentNode = oldParent

    def visit_ClassDef(self,node):
        classNode = Node(attributes = {'cyclomatic_complexity' : 1, 'type':'class','name':node.name,'start_line':node.lineno,'end_line':_get_last_line_number(node),'docstring':sanitize(ast.get_docstring(node,clean = True))},parent = self._parentNode)
        self._classes.append(classNode)
        oldParent = self._parentNode
        self._parentNode = classNode

        self._complexity_watchers.append(classNode)
        ast.NodeVisitor.generic_visit(self, node)
        self._complexity_watchers.remove(classNode)

        self._parentNode = oldParent

def analyze_file(filename):
    filepath = os.path.abspath(filename)
    with open(filepath,'r') as f:
        content = f.read()
    return analyze_string(content,filepath)

def analyze_string(string,path = ''):
    p = ast.parse(string,path,mode='exec')
    fileNode = Node(attributes = {'type':'file','path':path,'docstring':sanitize(ast.get_docstring(p,clean = True))})
    visitor = AnalysisNodeVisitor(parentNode = fileNode)
    visitor.visit(p)
    return (visitor,fileNode)

class Node(object):
    
    def __init__(self,attributes = {},parent = None):
        self.__dict__.update({'_children':[],'_parent':None,'_attributes':attributes})
        self.parent = parent

    def __repr__(self):
        return self.__class__.__name__+"(attributes = "+str(self.attributes)+", children = "+str(self.children)+")"

    @property
    def attributes(self):
        return self._attributes

    def __getitem__(self,name):
        return self.attributes[name]

    def __delitem__(self,name):
        del self.attributes[name]

    def __setitem__(self,name,value):
        self.attributes[name] = value
    
    @attributes.setter
    def attributes(self,attributes):
        """
        test
        """
        self._attributes = attributes
        
    @property
    def parent(self):
        return self._parent

    @parent.setter
    def parent(self,parent):
        if self._parent != None:
            self._parent.removeChild(self)
        self._parent = parent
        if self._parent != None:
            self._parent.addChild(self)

    def to_dict(self):
        if 'children' in self.attributes:
            raise Exception("Cannot export node that has a 'children' attribute!")
        result_dict = self.attributes.copy()
        result_dict['children'] = map(lambda child:child.to_dict(),self.children)
        return result_dict

    @property
    def children(self):
        return self._children
    
    def removeChild(self,child):
        if child in self.children():
            del self._children[self._children.indexof(child)]
            
    def addChild(self,child):
        if not child in self._children:
            self._children.append(child)
