import regex
from point import Point

empty = set()

class Marker(object):
    def __init__(self, name, level, y):
        self.name  = name
        self.level = level
        self.y     = y
    def __repr__(self):
        return '<Marker(%r, %r, %r)>' % (self.name, self.level, self.y)

class Tabber(object):
    wsre = regex.whitespace
    wst  = ('spaces', 'null', 'eol',)
    #sre  = regex.space
    sre  = regex.whitespace
    st   = ('spaces', 'null',)
    def __init__(self, m):
        self.mode  = m
        self.lines = {}

    def get_highlighter(self):
        return self.mode.window.buffer.highlights[self.mode.name]
    def get_tokens(self, y):
        return self.mode.window.buffer.highlights[self.mode.name].tokens[y]
    def get_token(self, y, i):
        return self.mode.window.buffer.highlights[self.mode.name].tokens[y][i]

    def token_is_whitespace(self, y, i):
        token = self.get_token(y, i)
        return token.fqname() in self.wst and self.wsre.match(token.string)
    def token_is_space(self, y, i):
        token = self.get_token(y, i)
        return token.fqname() in self.st and self.sre.match(token.string)

    def get_next_left_token(self, y, i):
        tokens = self.get_tokens(y)
        assert i >= 0 and i < len(tokens)
        for j in xrange(1, i):
            if not self.token_is_whitespace(y, i - j):
                return tokens[i - j]
        return None
    def get_next_right_token(self, y, i):
        tokens = self.get_tokens(y)
        assert i >= 0 and i < len(tokens)
        for j in xrange(i + 1, len(tokens)):
            if not self.token_is_whitespace(y, j):
                return tokens[j]
        return None
    def is_leftmost_token(self, y, i):
        return self.get_next_left_token(y, i) is None
    def is_rightmost_token(self, y, i):
        return self.get_next_right_token(y, i) is None
    def is_only_token(self, y, i):
        return self.is_leftmost_token(y, i) and self.is_rightmost_token(y, i)

    def get_leftmost_token(self, y):
        tokens = self.get_tokens(y)
        for i in xrange(0, len(tokens)):
            if not self.token_is_whitespace(y, i):
                return tokens[i]
        return None
    def get_rightmost_token(self, y):
        tokens = self.get_tokens(y)
        i = len(tokens) - 1
        for j in xrange(0, len(tokens)):
            if not self.token_is_whitespace(y, i - j):
                return tokens[i - j]
        return None

    def get_nonws_tokens(self, y):
        tokens = self.get_tokens(y)
        for i in xrange(0, len(tokens)):
            if not self.token_is_whitespace(y, i):
                yield tokens[i]
        raise StopIteration
    def get_nons_tokens(self, y):
        tokens = self.get_tokens(y)
        for i in xrange(0, len(tokens)):
            if not self.token_is_space(y, i):
                yield tokens[i]
        raise StopIteration

    def region_added(self, p, newlines):
        self.lines = {}
    def region_removed(self, p1, p2):
        self.lines = {}

    def is_base(self, y):
        return True
    def get_level(self, y):
        if y in self.lines:
            return self.lines[y]
        else:
            self._calc_level(y)
            return self.lines.get(y)
    def _calc_level(self, y):
        pass

class StackTabber(Tabber):
    def __init__(self, m):
        self.mode    = m
        self.lines   = {}
        self.record  = {}
        self.markers = []

    def get_curr_level(self):
        if self.markers:
            return self.markers[-1].level
        else:
            return 0

    def region_added(self, p, newlines):
        self.lines   = {}
        self.record  = {}
        self.markers = []
    def region_removed(self, p1, p2):
        self.lines   = {}
        self.record  = {}
        self.markers = []

    def is_base(self, y):
        return y == 0
    def _calc_level(self, y):
        # first we need to step back to find the last place where we have tab
        # stops figured out, or a suitable place to start
        target = y
        while not self.is_base(y) and y > 0:
            y -= 1

        # ok now, let's do this shit
        self.markers = []
        currlvl      = 0
        while y <= target:
            currlvl = self.get_curr_level()
            tokens = self.get_tokens(y)
            for i in xrange(0, len(tokens)):
                currlvl = self._handle_token(currlvl, y, i)
            self.lines[y]  = currlvl
            self.record[y] = tuple(self.markers)
            y += 1

    def _handle_token(self, currlvl, y, i):
        token = self.get_token(y, i)
        s = token.string
        fqname = token.fqname()

        if fqname in self.mode.closetokens and s in self.mode.closetags:
            currlvl = self._handle_close_token(currlvl, y, i)
        elif fqname in self.mode.opentokens and s in self.mode.opentags:
            currlvl = self._handle_open_token(currlvl, y, i)
        else:
            currlvl = self._handle_other_token(currlvl, y, i)
        return currlvl

    def _handle_open_token(self, currlvl, y, i):
        token  = self.get_token(y, i)
        rtoken = self.get_next_right_token(y, i)
        if rtoken is None:
            level = self.get_curr_level() + self.mode.tabwidth
        else:
            level = rtoken.x
        self._append(token.string, level, y)
        return currlvl
    def _handle_close_token(self, currlvl, y, i):
        token = self.get_token(y, i)
        s1 = token.string
        if not self.markers:
            raise Exception, "unmatched closing token %r" % s1
        s2 = self.markers[-1].name
        if self.mode.closetags[s1] == s2:
            self._pop()
            if self.is_leftmost_token(y, i):
                currlvl = self.get_curr_level()
        else:
            raise Exception, "mismatched closing tag %r vs %r" % (s2, s1)
        return currlvl
    def _handle_other_token(self, currlvl, y, i):
        return currlvl

    def _has_markers(self):
        return len(self.markers) > 0
    def _empty(self):
        return len(self.markers) == 0
    def _append(self, name, level, y=None):
        self.markers.append(Marker(name, level, y))
    def _peek(self):
        if self.markers:
            return self.markers[-1]
        else:
            return None
    def _peek_until(self, *names):
        for i in xrange(1, len(self.markers) + 1):
            x = self.markers[-i]
            if x.name in names:
                return x
        return None

    def _peek_name(self):
        if self.markers:
            return self.markers[-1].name
        else:
            return None
    def _peek_level(self):
        if self.markers:
            return self.markers[-1].level
        else:
            return None
    def _pop(self):
        self.markers.pop(-1)
    def _pop_while(self, *names):
        while self.markers and self.markers[-1].name in names:
            self.markers.pop(-1)
    def _pop_unless(self, *names):
        if self.markers and self.markers[-1].name not in names:
            self.markers.pop(-1)
    def _pop_until(self, *names):
        while self.markers:
            if self.markers[-1].name in names:
                self.markers.pop(-1)
                return
            else:
                self.markers.pop(-1)

    def _opt_append(self, name, level, y=None):
        if self.markers and self.markers[-1].name == name:
            pass
        else:
            self._append(name, level, y)
    def _opt_pop(self, *names):
        if self.markers and self.markers[-1].name in names:
            self.markers.pop(-1)

class Marker2(object):
    def __init__(self, name, type_, level, y=None):
        self.name  = name
        self.type_ = type_
        self.level = level
        self.y     = y
    def __repr__(self):
        return '<Marker2(%r, %r, %r, %r)>' % (self.name, self.type_, self.level, self.y)

class StackTabber2(Tabber):
    is_ignored_tokens  = ('spaces', 'eol', 'comment')
    is_indent_tokens   = ('spaces',)
    open_tokens        = {'delimiter': {'{': '}', '(': ')', '[': ']'}}
    close_tokens       = {'delimiter': set(['}', ')', ']'])}
    open_scope_tokens  = {'delimiter': set(['{'])}
    close_scope_tokens = {'delimiter': set(['}'])}
    open_test_tokens   = {'delimiter': set(['('])}
    close_test_tokens  = {'delimiter': set([')'])}
    control_tokens     = {'keyword': set(['if', 'else', 'while', 'do', 'for'])}
    case_tokens        = {'keyword': set(['case'])}
    case_delim_tokens  = {'delimiter': set([':'])}
    end_at_eof         = True
    end_at_tokens      = {}
    continue_tokens    = {}
    nocontinue_tokens  = {}
    start_free_tokens  = {'string.start': 'string.end'}
    end_free_tokens    = {'string.end': 'string.start'}
    start_macro_tokens = {}
    end_macro_tokens   = {}
    fixed_indent       = False
    def __init__(self, m):
        self.mode  = m
        self.name  = m.name
        self.lines = {}
        self._reset()
    def region_added(self, p, newlines):
        self.lines = {}
    def region_removed(self, p1, p2):
        self.lines = {}
    def get_level(self, y):
        if y not in self.lines:
            self._calc_level(y)
        return self.lines.get(y)

    def _calc_level(self, y):
        target = y
        while not self._is_base(y) and y > 0:
            y -= 1
        self._reset()
        while y <= target:
            self._save_curr_level()
            self._handle_tokens(y)
            y += 1

    def _is_base(self, y):
        return y == 0
        
    def _reset(self):
        self.record     = {}
        self.stack      = []
        self.markers    = self.stack
        self.curr_level = 0
    def _get_curr_level(self):
        if self.stack:
            return self.stack[-1].level
        else:
            return 0
    def _get_next_level(self):
        curr = self._get_curr_level()
        if curr is None: return None
        return curr + self.mode.tabwidth
    def _save_curr_level(self):
        self.curr_level = self._get_curr_level()

    def _match(self, *names):
        return self.stack and self.stack[-1].name in names
    def _nomatch(self, *names):
        return not self.stack or self.stack[-1].name not in names
    def _pop(self, *names):
        if self._match(*names):
            self.stack.pop()
    def _pop_while(self, *names):
        while self._match(*names):
            self.stack.pop()
    def _pop_until(self, *names):
        while self._nomatch(*names):
            self.stack.pop()

    def _append(self, name, type_, level, y=None):
        self.stack.append(Marker2(name, type_, level, y))
    def _append_unless(self, name, type_, level, y=None):
        if self._nomatch(name):
            self.stack.append(Marker2(name, type_, level, y))
    def _peek(self):
        if self.stack:
            return self.stack[-1]
        else:
            return None
    def _peek_until(self, *names):
        for i in xrange(1, len(self.stack) + 1):
            x = self.stack[-i]
            if x.name in names:
                return x
        return None

    def _get_tokens(self, y):
        return self.mode.window.buffer.highlights[self.name].tokens[y]
    def _handle_tokens(self, y):
        tokens = self._get_tokens(y)
        assert tokens
        start = int(self._is_indent(tokens[0]))
        end   = len(tokens) - 1

        while end > 0 and self._is_ignored(tokens[end]):
            end -= 1

        for i in xrange(0, end + 1 - start):
            t  = tokens[start + i]
            if self._is_ignored(t):
                pass
            elif self._is_close_token(t):
                self._handle_close_token(y, tokens, start, end, i, t)
            elif self._is_open_token(t):
                self._handle_open_token(y, tokens, start, end, i, t)
            else:
                self._handle_other_token(y, tokens, start, end, i, t)
        self.lines[y] = self.curr_level
        self.record[y] = tuple(self.stack)

    def _is_indent(self, t):
        return t.name in self.is_indent_tokens
    def _is_ignored(self, t):
        return t.name in self.is_ignored_tokens

    def _is_close_token(self, t):
        return t.string in self.close_tokens.get(t.name, empty)
    def _handle_close_token(self, y, tokens, start, end, i, t):
        while True:
            if not self.stack:
                raise Exception, "unmatched %r, line %d" % (t.string, y)
            marker = self.stack[-1]
            if marker.name in ('control', 'continue', 'pre-control'):
                self.stack.pop()
            elif marker.name == 'case':
                self.stack.pop()
            elif marker.name == 'case-eol':
                if t.string in self.close_scope_tokens.get(t.fqname(), empty):
                    self.stack.pop()
                else:
                    break
            elif marker.name in self.open_tokens.get(marker.type_, empty):
                s = self.open_tokens[marker.type_][marker.name]
                if s in (None, t.string):
                    self.stack.pop()
                    break
                else:
                    msg = "mismatched %r, line %d (expected %r)"
                    raise Exception, msg % (t.string, y, s)
            elif marker.name == 'pre-case':
                self.stack.pop()
            else:
                raise Exception, "what? %r (%r)" % (marker, t)

        # if we start a line with a closing token (e.g. "}") we may want to
        # shift the indentation left to "close" the indent. if we're using
        # fixed indentation then we may want to do this shift multiple times
        # (e.g. "})").
        if i == 0:
            self._save_curr_level()
        elif self.fixed_indent:
            all_closed = True
            for j in xrange(0, i):
                if self._is_ignored(tokens[j]) or self._is_close_token(tokens[j]):
                    pass
                else:
                    all_closed = False
                    break
            if all_closed:
                self._save_curr_level()

        at_eol = i + start == end

        # if we need to end at eof and we're at the EOF and we have a control
        # token, then we need to pop it.
        if self.end_at_eof and at_eol and self._match('control'):
            self.stack.pop()

        # if we are in a control test and this closes the test, then we need to
        # mark that we're out of the control test.
        if self._match('in-control') and \
            t.string in self.close_test_tokens.get(t.name, empty):
            self.stack[-1].name = 'control'

        # FIXME: we shouldn't be getting the type from the language
        if (self.end_at_eof and at_eol and self.stack and
            self.stack[-1].type_ == 'else'):
            self._pop_while('continue', 'control', 'pre-control')

        # if we don't want implicit continuation, return.
        if self.end_at_eof:
            return

        # ok, if we are closing a block then this will end a "single-statement"
        # block. e.g. else switch() { ... }
        if t.string in self.close_scope_tokens.get(t.fqname(), empty):
            self._pop('control')
            #self._pop('case')

        # if we do want implicit continuation, see if we need it.
        name, s = t.fqname(), t.string
        top     = self._peek()
        atscope = True
        if top:
            d = self.open_scope_tokens.get(top.type_, empty)
            atscope = top.name in d
        if (atscope and i + start == end):
            d = self.nocontinue_tokens.get(name)
            if d is None or d != 1 and s not in d:
                if s not in self.close_scope_tokens.get(name, empty):
                    nextlvl = self._get_next_level()
                    self._append_unless('continue', name, nextlvl, y)

    def _is_open_token(self, t):
        return t.string in self.open_tokens.get(t.name, empty)

    def _handle_open_token(self, y, tokens, start, end, i, t):
        if self.stack:
            if i == 0 and self._match('continue'):
                self.stack.pop()

        # if we have seen a control token, and we are starting a test, then
        # need to note that we've entered the test stanza.
        if self._match('pre-control'):
            if t.string in self.open_test_tokens.get(t.name, empty):
                self.stack[-1].name = 'in-control'
            else:
                self.stack.pop()

        open_scope = t.string in self.open_scope_tokens.get(t.name, empty)

        self._pop_while('case')

        if open_scope:
            self._pop('continue', 'control')
            if i == 0: self._save_curr_level()
        
        if i == end - start or self.fixed_indent:
            level = self._get_next_level()
        else:
            level = tokens[i + 1].x
            # i'm not sure why this is necessary; seems like there is a bug
            # somewhere else that this is compensating for.
            if self.curr_level > 0:
                level += 1
        self._append(t.string, t.name, level, y)

    def _handle_other_token(self, y, tokens, start, end, i, t):
        name, s = t.fqname(), t.string
        # handle "free" tokens (strings, heredocs, etc)
        if name in self.start_free_tokens:
            self._append('free', name, None, y)
            return
        elif name in self.end_free_tokens:
            self._pop('free')

        # handle macros
        if name in self.start_macro_tokens:
            self._append('macro', name, 0, y)
            if i == 0:
                self._save_curr_level()
            return
        elif name in self.end_macro_tokens:
            self._pop('macro')
            return

        at_eol = i + start == end

        # remove implicit continuation
        if self.end_at_eof and at_eol:
            self._pop_while('continue', 'control', 'pre-control')
        elif self.end_at_tokens.get(name, {}).get(s):
            self._pop_while('continue', 'control', 'pre-control')

        top = self._peek()

        # add implicit continuation
        if (at_eol and
            (top and top.name in self.open_scope_tokens.get(top.type_, {}) or
             not top)):
            if self.continue_tokens:
                if s in self.continue_tokens.get(name, {}):
                    self._append_unless('continue', name, self._get_next_level(), y)
            elif self.nocontinue_tokens:
                d = self.nocontinue_tokens.get(name)
                if d is None or d != 1 and s not in d:
                    self._append_unless('continue', name, self._get_next_level(), y)

        if name == 'continuation':
            # handle explicit continuation
            self._append_unless('continue', name, self._get_next_level(), y)
        elif s in self.control_tokens.get(name, empty):
            # handle control keywords
            if i == start:
                self._save_curr_level()
            self._pop_while('continue');
            self._append_unless('pre-control', s, self._get_next_level(), y)
        elif s in self.case_tokens.get(name, empty):
            if top is not None and top.name in ['case', 'case-eol']:
                self._pop('case', 'case-eol')
                if i == 0:
                    self._save_curr_level()
            self._append_unless('pre-case', name, self._get_next_level(), y)
        elif s in self.case_delim_tokens.get(name, empty):
            if top is not None and top.name == 'pre-case':
                if at_eol:
                    self.stack[-1].name = 'case-eol'
                    self.stack[-1].s = s
                else:
                    self.stack[-1].name = 'case'
                    self.stack[-1].s = s