import sublime
import sublime_plugin
from os.path import basename
import re


BH_TABSTOPS = re.compile(r"(\$\{BH_(SEL|TAB)(?:\:([^\}]+))?\})")
TAB_REGION = "bh_plugin_wrapping_tabstop"
SEL_REGION = "bh_plugin_wrapping_select"
OUT_REGION = "bh_plugin_wrapping_outlier"

VALID_INSERT_STYLES = (
    ("inline", "Inline Insert"),
    ("block", "Block Insert"),
    ("indent_block", "Indented Block Insert")
)


def exclude_entry(enabled, filter_type, language_list, language):
    """
    Exclude bracket wrapping entry by filter
    """

    exclude = True
    if enabled:
        # Black list languages
        if filter_type == 'blacklist':
            exclude = False
            if language != None:
                for item in language_list:
                    if language == item.lower():
                        exclude = True
                        break
        #White list languages
        elif filter_type == 'whitelist':
            if language != None:
                for item in language_list:
                    if language == item.lower():
                        exclude = False
                        break
    return exclude


class TextInsertion(object):
    """
    Wrapper class for inserting text
    """

    def __init__(self, view, edit):
        """
        Store view and edit objects
        """

        self.view = view
        self.edit = edit

    def insert(self, pt, text):
        """
        Peform insertion
        """

        return self.view.insert(self.edit, pt, text)


class WrapBrackets(object):
    """
    Wrap the current selection(s) with the defined wrapping options
    """

    def __init__(self, view, setting_file, attribute):
        self.view = view
        self._menu = []
        self._brackets = []
        self._insert = []
        self._style = []
        self.read_wrap_entries(setting_file, attribute)

    def inline(self, edit, sel):
        """
        Inline wrap
        """

        ti = TextInsertion(self.view, edit)

        offset1 = ti.insert(sel.begin(), self.brackets[0])
        self.insert_regions.append(sublime.Region(sel.begin(), sel.begin() + offset1))
        offset2 = ti.insert(sel.end() + offset1, self.brackets[1])
        self.insert_regions.append(sublime.Region(sel.end() + offset1, sel.end() + offset1 + offset2))

    def block(self, edit, sel, indent=False):
        """
        Wrap brackets around selection and block off the content
        """

        # Calculate number of lines between brackets
        self.calculate_lines(sel)
        # Calculate the current indentation of first bracket
        self.calculate_indentation(sel)

        ti = TextInsertion(self.view, edit)

        line_offset = 0
        first_end = 0
        second_end = 0
        second_start = sel.end()

        for b in reversed(self.brackets[1].split('\n')):
            second_end += ti.insert(sel.end(), "\n" + self.indent_to_col + b)
        num_open_lines = self.brackets[0].count('\n')
        for b in reversed(self.brackets[0].split('\n')):
            if line_offset == num_open_lines:
                line = b + "\n"
            else:
                line = self.indent_to_col + b + "\n"
            first_end += ti.insert(sel.begin(), line)
            line_offset += 1
        self.insert_regions.append(sublime.Region(sel.begin(), sel.begin() + first_end))

        if indent:
            second_start += self.indent_content(ti, line_offset)
        else:
            pt = self.view.text_point(self.first_line + line_offset, 0)
            second_start += ti.insert(pt, self.indent_to_col)

        self.insert_regions.append(sublime.Region(first_end + second_start, first_end + second_start + second_end))

    def indent_content(self, ti, line_offset):
        """
        Indent the block content
        """

        first = True
        offset = 0
        for l in range(line_offset, self.total_lines + line_offset):
            pt = self.view.text_point(self.first_line + l, 0)
            if first:
                offset += ti.insert(pt, self.indent_to_col + "\t")
                first = False
            else:
                offset += ti.insert(pt, "\t")
        return offset

    def calculate_lines(self, sel):
        """
        Calculate lines between brackets
        """

        self.first_line, self.col_position = self.view.rowcol(sel.begin())
        last_line = self.view.rowcol(sel.end())[0]
        self.total_lines = last_line - self.first_line + 1

    def calculate_indentation(self, sel):
        """
        Calculate how much lines should be indented
        """

        tab_size = self.view.settings().get("tab_size", 4)
        tab_count = self.view.substr(sublime.Region(sel.begin() - self.col_position, sel.begin())).count('\t')
        spaces = self.col_position - tab_count
        self.indent_to_col = "\t" * tab_count + "\t" * (spaces / tab_size) + " " * (spaces % tab_size if spaces >= tab_size else spaces)

    def select(self, edit):
        """
        Select defined regions after wrapping
        """

        self.view.sel().clear()
        map(lambda x: self.view.sel().add(x), self.insert_regions)

        final_sel = []
        initial_sel = []
        for s in self.view.sel():
            string = self.view.substr(s)
            matches = [m for m in BH_TABSTOPS.finditer(string)]
            multi_offset = 0
            if matches:
                for m in matches:
                    r = sublime.Region(s.begin() + multi_offset + m.start(1), s.begin() + multi_offset + m.end(1))
                    if m.group(3):
                        replace = m.group(3)
                        self.view.erase(edit, r)
                        added = self.view.insert(edit, r.begin(), replace)
                        final_sel.append(sublime.Region(s.begin() + multi_offset + m.start(1), s.begin() + multi_offset + m.start(1) + added))
                        multi_offset += added - r.size()
                    else:
                        self.view.erase(edit, r)
                        final_sel.append(sublime.Region(s.begin() + multi_offset + m.start(1)))
                        multi_offset -= r.size()
                    if m.group(2) == "SEL":
                        initial_sel.append(final_sel[-1])

        if len(initial_sel) != len(final_sel):
            self.view.add_regions(TAB_REGION, final_sel, "", "", sublime.HIDDEN)

        # Re-position cursor
        self.view.sel().clear()
        if len(initial_sel):
            map(lambda x: self.view.sel().add(x), initial_sel)
        elif len(final_sel):
            self.view.sel().add(final_sel[0])

    def read_wrap_entries(self, setting_file, attribute):
        """
        Read wrap entries from the settings file
        """

        settings = sublime.load_settings(setting_file)
        syntax = self.view.settings().get('syntax')
        language = basename(syntax).replace('.tmLanguage', '').lower() if syntax != None else "plain text"
        wrapping = settings.get(attribute, [])
        for i in wrapping:
            if not exclude_entry(i["enabled"], i["language_filter"], i["language_list"], language):
                for j in i.get("entries", []):
                    try:
                        menu_entry = j["name"]
                        bracket_entry = j["brackets"]
                        insert_style = j.get("insert_style", ["inline"])
                        self._menu.append(menu_entry)
                        self._brackets.append(bracket_entry)
                        self._insert.append(insert_style)
                    except Exception:
                        pass

    def wrap_brackets(self, value):
        """
        Wrap selection(s) with defined brackets
        """

        if value < 0:
            return

        # Use new edit object since the main run has already exited
        # and the old edit is more than likely closed now
        edit = self.view.begin_edit()

        # Wrap selections with brackets
        style = self._style[value]
        self.insert_regions = []

        for sel in self.view.sel():
            # Determine indentation style
            if style == "indent_block":
                self.block(edit, sel, True)
            elif style == "block":
                self.block(edit, sel)
            else:
                self.inline(edit, sel)

        self.select(edit)

        self.view.end_edit(edit)

    def wrap_style(self, value):
        """
        Choose insert style for wrapping.
        """

        if value < 0:
            return

        style = []

        self.brackets = self._brackets[value]
        for s in VALID_INSERT_STYLES:
            if s[0] in self._insert[value]:
                self._style.append(s[0])
                style.append(s[1])

        if len(style) > 1:
            self.view.window().show_quick_panel(
                style,
                self.wrap_brackets
            )
        else:
            self.wrap_brackets(0)


class WrapBracketsCommand(sublime_plugin.TextCommand, WrapBrackets):
    def run(self, edit):
        """
        Display the wrapping menu
        """

        self._menu = []
        self._brackets = []
        self._insert = []
        self._style = []
        self.read_wrap_entries("bh_wrapping.sublime-settings", "wrapping")

        if len(self._menu):
            self.view.window().show_quick_panel(
                self._menu,
                self.wrap_style
            )


class BhNextWrapSelCommand(sublime_plugin.TextCommand):
    """
    Navigate wrapping tab stop regions
    """

    def run(self, edit):
        """
        Look for the next wrapping tab stop region
        """

        regions = self.view.get_regions(SEL_REGION) + self.view.get_regions(OUT_REGION)
        if len(regions):
            self.view.sel().clear()
            map(lambda x: self.view.sel().add(x), regions)

        # Clean up unneed sections
        self.view.erase_regions(SEL_REGION)
        self.view.erase_regions(OUT_REGION)


class BhWrapListener(sublime_plugin.EventListener):
    """
    Listen for wrapping tab stop tabbing
    """

    def on_query_context(self, view, key, operator, operand, match_all):
        """
        Mark the next regions to navigate to.
        """

        accept_query = False
        if key == "bh_wrapping":
            select = []
            outlier = []
            regions = view.get_regions(TAB_REGION)
            tabstop = []
            sels = view.sel()

            if len(regions) == 0:
                return False

            for s in sels:
                count = 0
                found = False
                for r in regions[:]:
                    if found:
                        select.append(r)
                        tabstop.append(r)
                        del regions[count]
                        break
                    if r.begin() <= s.begin() <= r.end():
                        del regions[count]
                        found = True
                        continue
                    count += 1
                if not found:
                    outlier.append(s)
            tabstop += regions

            if len(tabstop) == len(select):
                if len(tabstop):
                    tabstop = []
                    accept_query = True
            elif len(tabstop) != 0:
                accept_query = True

            # Mark regions to make the "next" command aware of what to do
            view.add_regions(SEL_REGION, select, "", "", sublime.HIDDEN)
            view.add_regions(OUT_REGION, outlier, "", "", sublime.HIDDEN)
            view.add_regions(TAB_REGION, tabstop, "", "", sublime.HIDDEN)

        return accept_query
