import re
import textwrap
import unicodedata

import Default.comment
import sublime
import sublime_plugin


def previous_line(view, sr):
    """sr should be a Region covering the entire hard line"""
    if sr.begin() == 0:
        return None
    else:
        return view.full_line(sr.begin() - 1)


def next_line(view, sr):
    """sr should be a Region covering the entire hard line, including
    the newline"""
    if sr.end() == view.size():
        return None
    else:
        return view.full_line(sr.end())


separating_line_pattern = re.compile("^[\\t ]*\\n?$")


def is_paragraph_separating_line(view, sr):
    return separating_line_pattern.match(view.substr(sr)) is not None


def has_prefix(view, line, prefix):
    if not prefix:
        return True

    line_start = view.substr(sublime.Region(line.begin(), line.begin() + len(prefix)))

    return line_start == prefix


def expand_to_paragraph(view, tp):
    sr = view.full_line(tp)
    if is_paragraph_separating_line(view, sr):
        return sublime.Region(tp, tp)

    required_prefix = None

    # If the current line starts with a comment, only select lines that are also
    # commented
    (line_comments, block_comments) = Default.comment.build_comment_data(view, tp)
    dataStart = Default.comment.advance_to_first_non_white_space_on_line(view, sr.begin())
    for c in line_comments:
        (start, disable_indent) = c
        comment_region = sublime.Region(dataStart, dataStart + len(start))
        if view.substr(comment_region) == start:
            required_prefix = view.substr(sublime.Region(sr.begin(), comment_region.end()))
            break

    first = sr.begin()
    prev = sr
    while True:
        prev = previous_line(view, prev)
        if (prev is None or is_paragraph_separating_line(view, prev) or
                not has_prefix(view, prev, required_prefix)):
            break
        else:
            first = prev.begin()

    last = sr.end()
    next = sr
    while True:
        next = next_line(view, next)
        if (next is None or is_paragraph_separating_line(view, next) or
                not has_prefix(view, next, required_prefix)):
            break
        else:
            last = next.end()

    return sublime.Region(first, last)


def all_paragraphs_intersecting_selection(view, sr):
    paragraphs = []

    para = expand_to_paragraph(view, sr.begin())
    if not para.empty():
        paragraphs.append(para)

    while True:
        line = next_line(view, para)
        if line is None or line.begin() >= sr.end():
            break

        if not is_paragraph_separating_line(view, line):
            para = expand_to_paragraph(view, line.begin())
            paragraphs.append(para)
        else:
            para = line

    return paragraphs


class ExpandSelectionToParagraphCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        regions = []

        for s in self.view.sel():
            regions.append(sublime.Region(
                expand_to_paragraph(self.view, s.begin()).begin(),
                expand_to_paragraph(self.view, s.end()).end()))

        for r in regions:
            self.view.sel().add(r)


class WrapLinesCommand(sublime_plugin.TextCommand):
    def extract_prefix(self, sr):
        lines = self.view.split_by_newlines(sr)
        if len(lines) == 0:
            return None

        prefix = ''
        for char in self.view.substr(lines[0]):
            cat = unicodedata.category(char)[0]
            if ord(char) > 32 and cat != 'Z' and cat != 'P':
                break
            prefix += char

        if not prefix:
            return None

        for line in lines[1:]:
            if self.view.substr(sublime.Region(line.begin(), line.begin() + len(prefix))) != prefix:
                return None

        return prefix

    def width_in_spaces(self, str, tab_width):
        sum = 0
        for c in str:
            if c == '\t':
                sum += tab_width - 1
        return sum

    def run(self, edit, width=0):
        if width == 0 and self.view.settings().get("wrap_width"):
            try:
                width = int(self.view.settings().get("wrap_width"))
            except TypeError:
                pass

        if width == 0 and self.view.settings().get("rulers"):
            # try and guess the wrap width from the ruler, if any
            try:
                width = int(self.view.settings().get("rulers")[0])
            except ValueError:
                pass
            except TypeError:
                pass

        if width == 0:
            width = 78

        # Make sure tabs are handled as per the current buffer
        tab_width = 8
        if self.view.settings().get("tab_size"):
            try:
                tab_width = int(self.view.settings().get("tab_size"))
            except TypeError:
                pass

        if tab_width == 0:
            tab_width == 8

        paragraphs = []
        for s in self.view.sel():
            paragraphs.extend(all_paragraphs_intersecting_selection(self.view, s))

        if len(paragraphs) > 0:
            self.view.sel().clear()
            for p in paragraphs:
                self.view.sel().add(p)

            # This isn't an ideal way to do it, as we loose the position of the
            # cursor within the paragraph: hence why the paragraph is selected
            # at the end.
            for s in self.view.sel():
                wrapper = textwrap.TextWrapper()
                wrapper.expand_tabs = False
                wrapper.width = width
                prefix = self.extract_prefix(s)
                if prefix:
                    wrapper.initial_indent = prefix
                    wrapper.subsequent_indent = prefix
                    wrapper.width -= self.width_in_spaces(prefix, tab_width)

                if wrapper.width < 0:
                    continue

                txt = self.view.substr(s)
                if prefix:
                    txt = txt.replace(prefix, u"")

                txt = txt.expandtabs(tab_width)

                txt = wrapper.fill(txt) + u"\n"
                self.view.replace(edit, s, txt)

            # It's unhelpful to have the entire paragraph selected, just leave the
            # selection at the end
            ends = [s.end() - 1 for s in self.view.sel()]
            self.view.sel().clear()
            for pt in ends:
                self.view.sel().add(sublime.Region(pt))
