import commands, os.path, sets, string, sys, traceback
import color, completer, default, mode, method, regex, tab
from point import Point
from lex import Grammar, PatternRule, RegionRule, OverridePatternRule

class StringGrammar(Grammar):
    rules = [
        PatternRule(r'octal', r'\\[0-7]{3}'),
        PatternRule(r'escaped', r'\\.'),
    ]
class RawStringGrammar(Grammar):
    rules = [
        PatternRule(r'escaped', r'\\\\'),
        PatternRule(r'escaped', r"\\'"),
        PatternRule(r'escaped', r'\\"'),
    ]

class PythonGrammar(Grammar):
    rules = [
        PatternRule(r'functionname', r'(?<=def )[a-zA-Z_][a-zA-Z0-9_]*'),
        PatternRule(r'classname', r'(?<=class )[a-zA-Z_][a-zA-Z0-9_]*'),
        PatternRule(r'python_reserved', r'(?:True|None|False|Exception|self)(?![a-zA-Z0-9_])'),
        PatternRule(r'python_keyword', r'(?:yield|while|try|return|raise|print|pass|or|not|lambda|is|in|import|if|global|from|for|finally|exec|except|else|elif|del|def|continue|class|break|assert|as|and)(?![a-zA-Z0-9_])'),
        PatternRule(r"python_builtin", r'(?<!\.)(?:zip|xrange|vars|unicode|unichr|type|tuple|super|sum|str|staticmethod|sorted|slice|setattr|set|round|repr|reduce|raw_input|range|property|pow|ord|open|oct|object|max|min|map|long|locals|list|len|iter|issubclass|isinstance|int|input|id|hex|hash|hasattr|globals|getattr|frozenset|float|filter|file|execfile|eval|enumerate|divmod|dir|dict|delattr|complex|compile|coerce|cmp|classmethod|chr|callable|bool)(?![a-zA-Z0-9_])'),
        PatternRule(r'methodcall', r'(?<=\. )[a-zA-Z_][a-zA-Z0-9_]*(?= *\()'),
        PatternRule(r'functioncall', r'[a-zA-Z_][a-zA-Z0-9_]*(?= *\()'),
        PatternRule(r'system_identifier', r'__[a-zA-Z0-9_]+__'),
        PatternRule(r'private_identifier', r'__[a-zA-Z0-9_]*'),
        PatternRule(r'hidden_identifier', r'_[a-zA-Z0-9_]*'),

        RegionRule(r'rawstring', r'r"""', RawStringGrammar, r'"""'),
        RegionRule(r'rawstring', r"r'''", RawStringGrammar, r"'''"),
        RegionRule(r'rawstring', r'r"', RawStringGrammar, r'"'),
        RegionRule(r'rawstring', r"r'", RawStringGrammar, r"'"),
        RegionRule(r'string', r'u?"""', StringGrammar, r'"""'),
        RegionRule(r'string', r"u?'''", StringGrammar, r"'''"),
        RegionRule(r'string', r'u?"', StringGrammar, r'"'),
        RegionRule(r'string', r"u?'", StringGrammar, r"'"),

        PatternRule(r'identifier', r'[a-zA-Z_][a-zA-Z0-9_]*'),
        PatternRule(r'delimiter', r'\(|\)|\[|\]|{|}|@|,|:|\.|`|=|;|\+=|-=|\*=|/=|//=|%=|&=|\|=|\^=|>>=|<<=|\*\*='),
        PatternRule(r"integer", r"(?<![\.0-9a-zA-Z_])(?:0|-?[1-9][0-9]*|0[0-7]+|0[xX][0-9a-fA-F]+)[lL]?(?![\.0-9a-zA-Z_])"),
        PatternRule(r"float", r"(?<![\.0-9a-zA-Z_])(?:-?[0-9]+\.[0-9]*|-?\.[0-9]+|(?:[0-9]|[0-9]+\.[0-9]*|-?\.[0-9]+)[eE][\+-]?[0-9]+)(?![\.0-9a-zA-Z_])"),
        PatternRule(r"imaginary", r"(?<![\.0-9a-zA-Z_])(?:[0-9]+|(?:[0-9]+\.[0-9]*|\.[0-9]+|(?:[0-9]|[0-9]+\.[0-9]*|\.[0-9]+)[eE][\+-]?[0-9]+)[jJ])(?![\.0-9a-zA-Z_])"),

        PatternRule(r"operator", r"\+|<>|<<|<=|<|-|>>|>=|>|\*\*|&|\*|\||/|\^|==|//|~|!=|%"),

        OverridePatternRule(r'comment', r'#@@:(?P<token>[.a-zA-Z0-9_]+):(?P<mode>[.a-zA-Z0-9_]+) *$'),
        PatternRule(r'comment', r'#.*$'),
        PatternRule(r'continuation', r'\\\n$'),
        PatternRule(r'eol', r'\n$'),
    ]

class PythonTabber(tab.StackTabber):
    # NOTE: yield might initially seem like an endlevel name, but it's not one.
    endlevel_names   = ('pass', 'return', 'raise', 'break', 'continue')
    startlevel_names = ('if', 'try', 'class', 'def', 'for', 'while', 'try')
    def __init__(self, m):
        tab.StackTabber.__init__(self, m)
        self.base_level = 0

    def is_base(self, y):
        if y == 0:
            # we always know that line 0 is indented at the 0 level
            return True
        tokens = self.get_tokens(y)
        t0 = tokens[0]
        if t0.name == 'python_keyword' and t0.string in self.startlevel_names:
            # if a line has no whitespace and beings with something like
            # 'while','class','def','if',etc. then we can start at it
            return True
        else:
            # otherwise, we can't be sure that its level is correct
            return False

    def get_level(self, y):
        self._calc_level(y)
        return self.lines.get(y)

    def _calc_level(self, y):
        # ok, so first remember where we are going, and find our starting point
        target = y
        y = max(0, y - 1)
        while not self.is_base(y) and y > 0:
            y -= 1

        # ok, so clear out our stack and then loop over each line
        self.popped = False
        self.markers = []
        while y <= target:
            self.continued   = False
            self.last_popped = self.popped
            self.popped      = False
            tokens           = self.get_tokens(y)
            currlvl          = self.get_curr_level()
            # if we were continuing, let's pop that previous continuation token
            # and note that we're continuing
            if self.markers and self.markers[-1].name == 'cont':
                self.continued = True
                self._pop()
            # if we haven't reached the target-line yet, we can detect how many
            # levels of unindention, if any, the user chose on previous lines
            if y < target and len(tokens) > 2:
                if self.token_is_space(y, 0):
                    l = len(tokens[0].string)
                else:
                    l = 0
                while currlvl > l:
                    self._pop()
                    currlvl = self.get_curr_level()
                    self.popped = True
            # ok, having done all that, we can now process each token on the line
            for i in range(0, len(tokens)):
                currlvl = self._handle_token(currlvl, y, i)
            # so let's store the level for this line, as well as some debugging
            self.lines[y]  = currlvl
            self.record[y] = tuple(self.markers)
            y += 1

    def _handle_close_token(self, currlvl, y, i):
        try:
            return tab.StackTabber._handle_close_token(self, currlvl, y, i)
        except:
            return currlvl

    def _handle_other_token(self, currlvl, y, i):
        token  = self.get_token(y, i)
        fqname = token.fqname()
        if fqname == 'continuation':
            # we need to pop the indentation level over, unless last line was
            # also a continued line
            if self.continued:
                self._opt_append('cont', currlvl)
            else:
                self._opt_append('cont', currlvl + 4)
        elif fqname == 'string.start':
            # while inside of a string, there is no indention leve
            self._opt_append('string', None)
        elif fqname == 'string.end':
            # since we're done with the string, resume our indentation level
            self._opt_pop('string')
        elif fqname == 'delimiter':
            # we only really care about a colon as part of a one-line statement,
            # i.e.   "while ok: foo()" or "if True: print 3"
            if token.string == ':':
                if self.markers and self.markers[-1].name in ('[', '{'):
                    pass
                elif self.is_rightmost_token(y, i):
                    pass
                else:
                    #self._pop()
                    pass
        elif fqname == 'python_keyword':
            if token.string in self.endlevel_names:
                # we know we'll unindent at least once
                self._pop()
                self.popped = True
            elif token.string in self.startlevel_names and self.is_leftmost_token(y, i):
                # we know we will indent exactly once
                self._append(token.string, currlvl + 4)
            elif token.string in ('elif', 'else') and self.is_leftmost_token(y, i):
                # we know we'll unindent at least to the first if/elif
                if not self.popped and not self.last_popped:
                    self._pop_until('if', 'elif')
                    currlvl = self.get_curr_level()
                self._append(token.string, currlvl + 4)
            elif token.string == 'except' and self.is_leftmost_token(y, i):
                # we know we'll unindent at least to the first try
                if not self.popped and not self.last_popped:
                    self._pop_until('try')
                    currlvl = self.get_curr_level()
                self._append(token.string, currlvl + 4)
            elif token.string == 'finally' and self.is_leftmost_token(y, i):
                # we know we'll unindent at least to the first try/except
                if not self.popped and not self.last_popped:
                    self._pop_until('try', 'except')
                    currlvl = self.get_curr_level()
                self._append(token.string, currlvl + 4)
        return currlvl

class Python(mode.Fundamental):
    modename    = 'Python'
    extensions  = ['.py']
    detection   = ['python']
    tabbercls   = PythonTabber
    grammar     = PythonGrammar
    opentokens  = ('delimiter',)
    opentags    = {'(': ')', '[': ']', '{': '}'}
    closetokens = ('delimiter',)
    closetags   = {')': '(', ']': '[', '}': '{'}
    colors      = {
        'python_keyword':    ('cyan', 'default'),
        'python_reserved':   ('magenta', 'default'),
        'python_builtin':    ('cyan', 'default'),
        'functionname':      ('blue', 'default'),
        'classname':         ('green', 'default'),
        'rawstring.start':   ('green', 'default'),
        'rawstring.null':    ('green', 'default'),
        'rawstring.escaped': ('green', 'default'),
        'rawstring.end':     ('green', 'default'),
        'system_identifier': ('cyan', 'default'),
    }
    def __init__(self, w):
        mode.Fundamental.__init__(self, w)
        # tag matching
        self.add_bindings('close-paren', (')',))
        self.add_bindings('close-brace', ('}',))
        self.add_bindings('close-bracket', (']',))
        # add python-specific methods
        self.add_action_and_bindings(PythonCheckSyntax(), ('C-c s',))
        self.add_action_and_bindings(PythonDictCleanup(), ('C-c h',))
        # highlighting
        self.pythonlib = "."

class PythonSetLib(method.Method):
    '''Set the path(s) to find perl modules'''
    args = [method.Argument("lib", type=type(""), prompt="Python Path: ",
                            default=default.build_constant("."))]
    def _execute(self, w, **vargs):
        w.mode.pythonlib = vargs['lib']
    
class PythonCheckSyntax(method.Method):
    '''Check the syntax of the current python file'''
    def _execute(self, w, **vargs):
        sys.path.insert(0, w.mode.pythonlib)
        source = w.buffer.make_string()
        try:
            code = compile(source, w.buffer.path, 'exec')
            w.set_error("Syntax OK")
        except Exception, e:
            output = traceback.format_exc()
            w.application.data_buffer("*PythonSyntax*", output, switch_to=True)
        del sys.path[0]

class PythonDictCleanup(method.Method):
    '''Align assignment blocks and literal dictionaries'''
    def _execute(self, w, **vargs):
        cursor = w.logical_cursor()
        b = w.buffer

        # so this is where we will store the groups that we find
        groups_by_line = {}

        # the regex we will try
        regexes = [regex.python_dict_cleanup,
                   regex.python_assign_cleanup]

        # if we aren't in a hash, inform the user and exit
        line = b.lines[cursor.y]
        myregex = None
        for r in regexes:
            if r.match(line):
                myregex = r

        if myregex is None:
            raise Exception, "Not a python dict line"

        groups_by_line[cursor.y] = myregex.match(line).groups()
        
        # find the beginning of this hash block
        start = 0
        i = cursor.y - 1
        while i >= 0:
            line = b.lines[i]
            m = myregex.match(line)
            if not m:
                start = i + 1
                break
            else:
                groups_by_line[i] = m.groups()
            i -= 1

        # find the end of this hash block
        end = len(b.lines) - 1
        i = cursor.y + 1
        while i < len(b.lines):
            line = b.lines[i]
            m = myregex.match(line)
            if not m:
                end = i - 1
                break
            else:
                groups_by_line[i] = m.groups()
            i += 1
    
        # assume that the least indented line is correct
        indent_w = min([len(groups_by_line[k][0]) for k in groups_by_line])

        # find the longest hash key to base all the other padding on
        key_w = max([len(groups_by_line[k][1]) for k in groups_by_line])

        # for each line, format it correctly
        keys = groups_by_line.keys()
        keys.sort()
        data = ''
        for i in keys:
            indent_pad = ' ' * indent_w
            key = groups_by_line[i][1]
            sep = groups_by_line[i][3]
            value = groups_by_line[i][5]
            key_pad = ' ' * (key_w - len(key))
            if sep == '=':
                data += indent_pad + key + key_pad + ' ' + sep + ' ' + value + '\n'
            else:
                data += indent_pad + key + sep + ' ' + key_pad + value + '\n'

        # remove the old text and add the new
        start_p = Point(0, start)
        if end + 1 < len(w.buffer.lines):
            end_p = Point(0, end + 1)
        else:
            end_p = Point(len(w.buffer.lines[-1]), len(w.buffer.lines) - 1)
        w.kill(start_p, end_p)
        w.insert_string(start_p, data)

install = Python.install