"""
Wrapper module around a Sublime Text 3 view for showing a terminal look-a-like
"""
import collections
import time

import sublime
import sublime_plugin

from . import gateone_terminal_emulator
from . import pyte_terminal_emulator
from . import utils
from . import sublime_view_cache


class SublimeBufferManager():
    """
    A manager to control all SublimeBuffer instances so they can be looked up
    based on the sublime view they are governing.
    """
    @classmethod
    def register(cls, uid, sublime_buffer):
        if not hasattr(cls, "buffers"):
            cls.buffers = {}
        cls.buffers[uid] = sublime_buffer

    @classmethod
    def deregister(cls, uid):
        if hasattr(cls, "buffers"):
            del cls.buffers[uid]

    @classmethod
    def load_from_id(cls, uid):
        if hasattr(cls, "buffers") and uid in cls.buffers:
            return cls.buffers[uid]
        else:
            raise Exception("[terminal_view error] Sublime buffer not found")


class SublimeTerminalBuffer():
    def __init__(self, sublime_view, title, syntax_file=None):
        self._view = sublime_view
        self._view.set_name(title)
        self._view.set_scratch(True)
        self._view.set_read_only(True)
        self._view.settings().set("gutter", False)
        self._view.settings().set("highlight_line", False)
        self._view.settings().set("auto_complete_commit_on_tab", False)
        self._view.settings().set("draw_centered", False)
        self._view.settings().set("word_wrap", False)
        self._view.settings().set("auto_complete", False)
        self._view.settings().set("draw_white_space", "none")
        self._view.settings().set("draw_indent_guides", False)
        self._view.settings().set("caret_style", "blink")
        self._view.settings().set("scroll_past_end", False)
        self._view.settings().add_on_change('color_scheme', lambda: set_color_scheme(self._view))

        if syntax_file is not None:
            self._view.set_syntax_file("Packages/User/" + syntax_file)

        # Mark in the views private settings that this is a terminal view so we
        # can use this as context in the keymap
        self._view.settings().set("terminal_view", True)

        settings = sublime.load_settings('TerminalView.sublime-settings')
        self._show_colors = settings.get("terminal_view_show_colors", False)
        self._right_margin = settings.get("terminal_view_right_margin", 3)
        self._bottom_margin = settings.get("terminal_view_bottom_margin", 0)

        # Use pyte as underlying terminal emulator
        hist = settings.get("terminal_view_scroll_history", 1000)
        ratio = settings.get("terminal_view_scroll_ratio", 0.5)
        self._term_emulator = pyte_terminal_emulator.PyteTerminalEmulator(80, 24, hist, ratio)

        self._keypress_callback = None
        self._view_content_cache = sublime_view_cache.SublimeViewContentCache()
        self._view_region_cache = sublime_view_cache.SublimeViewRegionCache()

        # Register the new instance of the sublime buffer class so other
        # commands can look it up when they are called in the same sublime view
        SublimeBufferManager.register(sublime_view.id(), self)

    def __del__(self):
        utils.ConsoleLogger.log("Sublime buffer instance deleted")

    def set_keypress_callback(self, callback):
        self._keypress_callback = callback

    def keypress_callback(self):
        return self._keypress_callback

    def view_region_cache(self):
        return self._view_region_cache

    def view_content_cache(self):
        return self._view_content_cache

    def colors_enabled(self):
        return self._show_colors

    def terminal_emulator(self):
        return self._term_emulator

    def insert_data(self, data):
        start = time.time()
        self._term_emulator.feed(data)
        t = time.time() - start
        utils.ConsoleLogger.log("Updated terminal emulator in %.3f ms" % (t * 1000.))

    def update_view(self):
        self._scroll_terminal_if_requested()
        if self.terminal_emulator().modified():
            self._view.run_command("terminal_view_update")

    def is_open(self):
        return self._view.is_valid()

    def deactivate(self):
        self._view.settings().set("terminal_view", False)
        self.update_view()
        self._keypress_callback = None
        SublimeBufferManager.deregister(self._view.id())

    def close(self):
        if self.is_open():
            sublime.active_window().focus_view(self._view)
            sublime.active_window().run_command("close_file")

    def update_terminal_size(self, nb_rows, nb_cols):
        # Make sure all content beyond the new number of rows is deleted
        if nb_rows < self._term_emulator.nb_lines():
            start, _ = self.view_content_cache().get_line_start_and_end_points(nb_rows)
            self._view.run_command("terminal_view_clear", args={"start": start})

        self._term_emulator.resize(nb_rows, nb_cols)

    def view_size(self):
        view = self._view
        (pixel_width, pixel_height) = view.viewport_extent()
        pixel_per_line = view.line_height()
        pixel_per_char = view.em_width()

        if pixel_per_line == 0 or pixel_per_char == 0:
            return (0, 0)

        # Subtract one to avoid any wrapping issues
        nb_columns = int(pixel_width / pixel_per_char) - self._right_margin
        if nb_columns < 1:
            nb_columns = 1

        nb_rows = int(pixel_height / pixel_per_line) - self._bottom_margin
        if nb_rows < 1:
            nb_rows = 1

        return (nb_rows, nb_columns)

    def _scroll_terminal_if_requested(self):
        scroll_request = self._view.settings().get("terminal_view_scroll", None)
        if scroll_request is not None:
            index = scroll_request[0]
            direction = scroll_request[1]
            if index == "line":
                if direction == "up":
                    self.terminal_emulator().prev_line()
                else:
                    self.terminal_emulator().next_line()
            else:
                if direction == "up":
                    self.terminal_emulator().prev_page()
                else:
                    self.terminal_emulator().next_page()

            self._view.settings().set("terminal_view_scroll", None)


class TerminalViewScroll(sublime_plugin.TextCommand):
    def run(self, _, forward=False, line=False):
        # Mark in view to request a scroll in the thread that handles the
        # updates. Note lines are NOT supported at the moment.
        if line:
            scroll_request = ("line", )
        else:
            scroll_request = ("page", )

        if not forward:
            scroll_request = scroll_request + ("up", )
        else:
            scroll_request = scroll_request + ("down", )

        self.view.settings().set("terminal_view_scroll", scroll_request)


class TerminalViewKeypress(sublime_plugin.TextCommand):
    def __init__(self, view):
        super().__init__(view)
        self._sub_buffer = None

    def run(self, _, **kwargs):
        # Lookup the sublime buffer instance for this view the first time this
        # command is called
        if self._sub_buffer is None:
            self._sub_buffer = SublimeBufferManager.load_from_id(self.view.id())

        if type(kwargs["key"]) is not str:
            sublime.error_message("Terminal View: Got keypress with non-string key")
            return

        if "meta" in kwargs and kwargs["meta"]:
            sublime.error_message("Terminal View: Meta key is not supported yet")
            return

        if "meta" not in kwargs:
            kwargs["meta"] = False
        if "alt" not in kwargs:
            kwargs["alt"] = False
        if "ctrl" not in kwargs:
            kwargs["ctrl"] = False
        if "shift" not in kwargs:
            kwargs["shift"] = False

        # Lookup the sublime buffer instance for this view
        sublime_buffer = SublimeBufferManager.load_from_id(self.view.id())
        keypress_cb = sublime_buffer.keypress_callback()
        app_mode = sublime_buffer.terminal_emulator().application_mode_enabled()
        if keypress_cb:
            keypress_cb(kwargs["key"], kwargs["ctrl"], kwargs["alt"],
                        kwargs["shift"], kwargs["meta"], app_mode)


class TerminalViewCopy(sublime_plugin.TextCommand):
    def run(self, edit):
        # Get selected region or use line that cursor is on if nothing is
        # selected
        selected_region = self.view.sel()[0]
        if selected_region.empty():
            selected_region = self.view.line(selected_region)

        # Clean the selected text and move it into clipboard
        selected_text = self.view.substr(selected_region)
        selected_lines = selected_text.split("\n")
        clean_contents_to_copy = ""
        for line in selected_lines:
            clean_contents_to_copy = clean_contents_to_copy + line.rstrip() + "\n"

        sublime.set_clipboard(clean_contents_to_copy[:-1])


class TerminalViewPaste(sublime_plugin.TextCommand):
    def run(self, edit, bracketed=False):
        # Lookup the sublime buffer instance for this view
        sub_buffer = SublimeBufferManager.load_from_id(self.view.id())
        keypress_cb = sub_buffer.keypress_callback()
        if not keypress_cb:
            return

        # Check if bracketed paste mode is enabled
        bracketed = bracketed or sub_buffer.terminal_emulator().bracketed_paste_mode_enabled()
        if bracketed:
            keypress_cb("bracketed_paste_mode_start")

        copied = sublime.get_clipboard()
        copied = copied.replace("\r\n", "\n")
        for char in copied:
            if char == "\n" or char == "\r":
                keypress_cb("enter")
            elif char == "\t":
                keypress_cb("tab")
            else:
                keypress_cb(char)

        if bracketed:
            keypress_cb("bracketed_paste_mode_end")


class TerminalViewReporter(sublime_plugin.EventListener):
    def on_query_context(self, view, key, operator, operand, match_all):
        if key == "terminal_view_needs_refocus":
            cursor_pos = view.settings().get("terminal_view_last_cursor_pos")
            if cursor_pos:
                if len(view.sel()) != 1 or not view.sel()[0].empty():
                    return operand

                row, col = view.rowcol(view.sel()[0].end())
                return (row == cursor_pos[0] and col == cursor_pos[1]) != operand


class TerminalViewRefocus(sublime_plugin.TextCommand):
    def run(self, _):
        cursor_pos = self.view.settings().get("terminal_view_last_cursor_pos")
        tp = self.view.text_point(cursor_pos[0], cursor_pos[1])
        self.view.sel().clear()
        self.view.sel().add(sublime.Region(tp, tp))


class TerminalViewUpdate(sublime_plugin.TextCommand):
    def __init__(self, view):
        super().__init__(view)
        self._sub_buffer = None

    def run(self, edit):
        # Lookup the sublime buffer instance for this view the first time this
        # command is called
        if self._sub_buffer is None:
            self._sub_buffer = SublimeBufferManager.load_from_id(self.view.id())

        # Update dirty lines in buffer if there are any
        dirty_lines = self._sub_buffer.terminal_emulator().dirty_lines()
        if len(dirty_lines) > 0:
            self._update_viewport_position()

            # Invalidate the last cursor position when dirty lines are updated
            self.view.settings().set("terminal_view_last_cursor_pos", None)

            # Generate color map
            color_map = {}
            if self._sub_buffer.colors_enabled():
                start = time.time()
                color_map = self._sub_buffer.terminal_emulator().color_map(dirty_lines.keys())
                t = time.time() - start
                utils.ConsoleLogger.log("Generated color map in %.3f ms" % (t * 1000.))

            # Update the view
            start = time.time()
            self._update_lines(edit, dirty_lines, color_map)
            t = time.time() - start
            utils.ConsoleLogger.log("Updated ST3 view in %.3f ms" % (t * 1000.))

        # Update cursor last to avoid a selection blinking at the top of the
        # terminal when starting or when a new prompt is being drawn at the
        # bottom
        self._update_cursor()

        # Clear dirty lines (and modified flag)
        self._sub_buffer.terminal_emulator().clear_dirty()

    def _update_viewport_position(self):
        self.view.set_viewport_position((0, 0), animate=False)

    def _update_cursor(self):
        cursor_pos = self._sub_buffer.terminal_emulator().cursor()
        last_cursor_pos = self.view.settings().get("terminal_view_last_cursor_pos")
        if last_cursor_pos and last_cursor_pos[0] == cursor_pos[0] and last_cursor_pos[1] == cursor_pos[1]:
            return

        tp = self.view.text_point(cursor_pos[0], cursor_pos[1])
        self.view.sel().clear()
        self.view.sel().add(sublime.Region(tp, tp))
        self.view.settings().set("terminal_view_last_cursor_pos", cursor_pos)

    def _update_lines(self, edit, dirty_lines, color_map):
        self.view.set_read_only(False)
        lines = dirty_lines.keys()
        for line_no in sorted(lines):
            # Clear any colors on the line
            self._remove_color_regions_on_line(line_no)

            # Update the line
            self._update_line_content(edit, line_no, dirty_lines[line_no])

            # Apply colors to the line if there are any on it
            if line_no in color_map:
                self._update_line_colors(line_no, color_map[line_no])

        self.view.set_read_only(True)

    def _remove_color_regions_on_line(self, line_no):
        view_region_cache = self._sub_buffer.view_region_cache()
        if view_region_cache.has_line(line_no):
            region_keys = view_region_cache.get_line(line_no)
            for key in region_keys:
                self.view.erase_regions(key)
            view_region_cache.delete_line(line_no)

    def _update_line_content(self, edit, line_no, content):
        # Note this function has been optimized quite a bit. Calls to the ST3
        # API has been left out on purpose as they are slower than the
        # alternative.

        # We need to add a newline otherwise ST3 does not break the line
        content_w_newline = content + "\n"

        # Check in our local buffer that the content line is different from what
        # we are already showing - otherwise we can stop now
        view_content_cache = self._sub_buffer.view_content_cache()
        if view_content_cache.has_line(line_no):
            if view_content_cache.get_line(line_no) == content_w_newline:
                return

        # Content is different - make ST3 region that spans the line. Start by
        # geting start and end point of the line
        line_start, line_end = view_content_cache.get_line_start_and_end_points(line_no)

        # Make region spanning entire line (including any newline at the end)
        line_region = sublime.Region(line_start, line_end)
        self.view.replace(edit, line_region, content_w_newline)
        view_content_cache.update_line(line_no, content_w_newline)

    def _update_line_colors(self, line_no, line_color_map):
        # Note this function has been optimized quite a bit. Calls to the ST3
        # API has been left out on purpose as they are slower than the
        # alternative.
        view_region_cache = self._sub_buffer.view_region_cache()
        view_content_cache = self._sub_buffer.view_content_cache()

        for idx, field in line_color_map.items():
            length = field["field_length"]
            color_scope = "terminalview.%s_%s" % (field["color"][0], field["color"][1])

            # Get text point where color should start
            line_start, _ = view_content_cache.get_line_start_and_end_points(line_no)
            color_start = line_start + idx

            # Make region that should be colored
            buffer_region = sublime.Region(color_start, color_start + length)
            region_key = "%i,%s" % (line_no, idx)

            # Add the region
            flags = sublime.DRAW_NO_OUTLINE | sublime.PERSISTENT
            self.view.add_regions(region_key, [buffer_region], color_scope, flags=flags)
            view_region_cache.add(line_no, region_key)


class TerminalViewClear(sublime_plugin.TextCommand):
    def run(self, edit, start=0, end=None):
        if end is None:
            end = self.view.size()

        self.view.set_read_only(False)
        region = sublime.Region(start, end)
        self.view.erase(edit, region)
        self.view.set_read_only(True)


def set_color_scheme(view):
    """
    Set color scheme for view
    """
    color_scheme = "Packages/TerminalView/TerminalView.hidden-tmTheme"

    # Check if user color scheme exists
    try:
        sublime.load_resource("Packages/User/TerminalView.hidden-tmTheme")
        color_scheme = "Packages/User/TerminalView.hidden-tmTheme"
    except:
        pass

    if view.settings().get('color_scheme') != color_scheme:
        view.settings().set('color_scheme', color_scheme)