import sublime
import sublime_plugin
from .core.completion import parse_completion_response, format_completion
from .core.configurations import is_supported_syntax
from .core.documents import position_is_word
from .core.edit import parse_text_edit
from .core.logging import debug
from .core.protocol import Request
from .core.registry import session_for_view, client_from_session, LSPViewEventListener
from .core.sessions import Session
from .core.settings import settings, client_configs
from .core.typing import Any, List, Dict, Tuple, Optional, Union
from .core.views import text_document_position_params


class CompletionState(object):
    IDLE = 0
    REQUESTING = 1
    APPLYING = 2
    CANCELLING = 3


last_text_command = None


class CompletionHelper(sublime_plugin.EventListener):
    def on_text_command(self, view: sublime.View, command_name: str, args: Optional[Any]) -> None:
        global last_text_command
        last_text_command = command_name


class LspTrimCompletionCommand(sublime_plugin.TextCommand):

    def run(self, edit: sublime.Edit, range: Optional[Tuple[int, int]] = None) -> None:
        if range:
            start, end = range
            region = sublime.Region(start, end)
            self.view.erase(edit, region)


class CompletionHandler(LSPViewEventListener):
    def __init__(self, view: sublime.View) -> None:
        super().__init__(view)
        self.initialized = False
        self.enabled = False
        self.trigger_chars = []  # type: List[str]
        self.auto_complete_selector = ""
        self.resolve = False
        self.state = CompletionState.IDLE
        self.completions = []  # type: List[Any]
        self.next_request = None  # type: Optional[Tuple[str, List[int]]]
        self.last_prefix = ""
        self.last_location = -1
        self.committing = False
        self.response_items = []  # type: List[dict]
        self.response_incomplete = False

    @classmethod
    def is_applicable(cls, view_settings: dict) -> bool:
        if 'completion' in settings.disabled_capabilities:
            return False

        syntax = view_settings.get('syntax')
        return is_supported_syntax(syntax, client_configs.all) if syntax else False

    def initialize(self) -> None:
        self.initialized = True
        session = session_for_view(self.view, 'completionProvider')
        if session:
            completionProvider = session.get_capability('completionProvider') or dict()  # type: dict
            # A language server may have an empty dict as CompletionOptions. In that case,
            # no trigger characters will be registered but we'll still respond to Sublime's
            # usual query for completions. So the explicit check for None is necessary.
            self.enabled = True
            self.resolve = completionProvider.get('resolveProvider') or False
            self.trigger_chars = completionProvider.get(
                'triggerCharacters') or []
            if self.trigger_chars:
                self.register_trigger_chars(session)
            self.auto_complete_selector = self.view.settings().get("auto_complete_selector", "") or ""

    def _view_language(self, config_name: str) -> Optional[str]:
        languages = self.view.settings().get('lsp_language')
        return languages.get(config_name) if languages else None

    def register_trigger_chars(self, session: Session) -> None:
        completion_triggers = self.view.settings().get('auto_complete_triggers', []) or []  # type: List[Dict[str, str]]
        view_language = self._view_language(session.config.name)
        if view_language:
            for language in session.config.languages:
                if language.id == view_language:
                    for scope in language.scopes:
                        # debug("registering", self.trigger_chars, "for", scope)
                        scope_trigger = next(
                            (trigger for trigger in completion_triggers if trigger.get('selector', None) == scope),
                            None
                        )
                        if not scope_trigger:  # do not override user's trigger settings.
                            completion_triggers.append({
                                'characters': "".join(self.trigger_chars),
                                'selector': scope
                            })

            self.view.settings().set('auto_complete_triggers', completion_triggers)

    def is_after_trigger_character(self, location: int) -> bool:
        if location > 0:
            prev_char = self.view.substr(location - 1)
            return prev_char in self.trigger_chars
        else:
            return False

    def is_same_completion(self, prefix: str, locations: List[int]) -> bool:
        if self.response_incomplete:
            return False

        if self.last_location < 0:
            return False

        # completion requests from the same location with the same prefix are cached.
        current_start = locations[0] - len(prefix)
        last_start = self.last_location - len(self.last_prefix)
        return prefix.startswith(self.last_prefix) and current_start == last_start

    def find_completion_item(self, inserted: str) -> Optional[dict]:
        """

        Returns the completionItem for a given replacement string.
        Matches exactly or up to first snippet placeholder ($s)

        """
        # TODO: candidate for extracting and thorough testing.
        if self.completions:
            for index, item in enumerate(self.completions):
                trigger, replacement = item

                snippet_offset = replacement.find('$', 2)
                if snippet_offset > -1:
                    if inserted.startswith(replacement[:snippet_offset]):
                        return self.response_items[index]
                else:
                    if replacement == inserted:
                        return self.response_items[index]
        return None

    def on_modified(self) -> None:

        # hide completion when backspacing past last completion.
        if self.view.sel()[0].begin() < self.last_location:
            self.last_location = -1
            self.view.run_command("hide_auto_complete")

        # cancel current completion if the previous input is an space
        prev_char = self.view.substr(self.view.sel()[0].begin() - 1)
        if self.state == CompletionState.REQUESTING and prev_char.isspace():
            self.state = CompletionState.CANCELLING

        if self.committing:
            self.committing = False
            self.on_completion_inserted()
        else:
            if self.view.is_auto_complete_visible():
                if self.response_incomplete:
                    # debug('incomplete, triggering new completions')
                    self.view.run_command("hide_auto_complete")
                    sublime.set_timeout(self.run_auto_complete, 0)

    def on_completion_inserted(self) -> None:
        # get text inserted from last completion
        begin = self.last_location

        if begin < 0:
            return

        if position_is_word(self.view, begin):
            word = self.view.word(self.last_location)
            begin = word.begin()

        region = sublime.Region(begin, self.view.sel()[0].end())
        inserted = self.view.substr(region)

        item = self.find_completion_item(inserted)
        if not item:
            # issues 714 and 720 - calling view.word() on last_location includes a trigger char that is not part of
            # inserted completion.
            debug('No match for inserted "{}", skipping first char'.format(inserted))
            begin += 1
            item = self.find_completion_item(inserted[1:])

        if item:
            # the newText is already inserted, now we need to check where it should start.
            edit = item.get('textEdit')
            if edit:
                parsed_edit = parse_text_edit(edit)
                start, end, newText, version = parsed_edit
                edit_start_loc = self.view.text_point(*start)

                # if the edit started before the word, we need to trim back to the start of the edit.
                if edit_start_loc < begin:
                    trim_range = (edit_start_loc, begin)
                    debug('trimming between', trim_range, 'because textEdit', parsed_edit)
                    self.view.run_command("lsp_trim_completion", {'range': trim_range})

            # import statements, etc. some servers only return these after a resolve.
            additional_edits = item.get('additionalTextEdits')
            if additional_edits:
                self.apply_additional_edits(additional_edits)
            elif self.resolve:
                self.do_resolve(item)

        else:
            debug('could not find completion item for inserted "{}"'.format(inserted))

    def match_selector(self, location: int) -> bool:
        return self.view.match_selector(location, self.auto_complete_selector)

    def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[Tuple[List[Tuple[str, str]], int]]:
        if not self.initialized:
            self.initialize()

        flags = 0
        if settings.only_show_lsp_completions:
            flags |= sublime.INHIBIT_WORD_COMPLETIONS
            flags |= sublime.INHIBIT_EXPLICIT_COMPLETIONS

        if self.enabled:
            if not self.match_selector(locations[0]):
                return ([], flags)

            reuse_completion = self.is_same_completion(prefix, locations)
            if self.state == CompletionState.IDLE:
                if not reuse_completion:
                    self.last_prefix = prefix
                    self.last_location = locations[0]
                    self.do_request(prefix, locations)
                    self.completions = []

            elif self.state in (CompletionState.REQUESTING, CompletionState.CANCELLING):
                if not reuse_completion:
                    self.next_request = (prefix, locations)
                    self.state = CompletionState.CANCELLING

            elif self.state == CompletionState.APPLYING:
                self.state = CompletionState.IDLE

            return (self.completions, flags)

        return None

    def on_text_command(self, command_name: str, args: Optional[Any]) -> None:
        self.committing = command_name in ('commit_completion', 'auto_complete')

    def do_request(self, prefix: str, locations: List[int]) -> None:
        self.next_request = None
        view = self.view

        # don't store client so we can handle restarts
        client = client_from_session(session_for_view(view, 'completionProvider', locations[0]))
        if not client:
            return

        if settings.complete_all_chars or self.is_after_trigger_character(locations[0]):
            self.manager.documents.purge_changes(self.view)
            document_position = text_document_position_params(self.view, locations[0])
            self.state = CompletionState.REQUESTING
            client.send_request(
                Request.complete(document_position),
                self.handle_response,
                self.handle_error)

    def do_resolve(self, item: dict) -> None:
        view = self.view

        client = client_from_session(session_for_view(view, 'completionProvider', self.last_location))
        if not client:
            return

        client.send_request(Request.resolveCompletionItem(item), self.handle_resolve_response)

    def handle_resolve_response(self, response: Optional[Dict]) -> None:
        if response:
            additional_edits = response.get('additionalTextEdits')
            if additional_edits:
                self.apply_additional_edits(additional_edits)

    def apply_additional_edits(self, additional_edits: List[Dict]) -> None:
        edits = list(parse_text_edit(additional_edit) for additional_edit in additional_edits)
        debug('applying additional edits:', edits)
        self.view.run_command("lsp_apply_document_edit", {'changes': edits})
        sublime.status_message('Applied additional edits for completion')

    def handle_response(self, response: Optional[Union[Dict, List]]) -> None:
        if self.state == CompletionState.REQUESTING:

            completion_start = self.last_location
            if position_is_word(self.view, self.last_location):
                # if completion is requested in the middle of a word, where does it start?
                word = self.view.word(self.last_location)
                completion_start = word.begin()

            current_word_start = self.view.sel()[0].begin()
            if position_is_word(self.view, current_word_start):
                current_word_region = self.view.word(current_word_start)
                current_word_start = current_word_region.begin()

            if current_word_start != completion_start:
                debug('completion results for', completion_start, 'now at', current_word_start, 'discarding')
                self.state = CompletionState.IDLE
                return

            _last_row, last_col = self.view.rowcol(completion_start)

            response_items, response_incomplete = parse_completion_response(response)
            self.response_items = response_items
            self.response_incomplete = response_incomplete
            self.completions = list(format_completion(item, last_col, settings) for item in self.response_items)

            # if insert_best_completion was just ran, undo it before presenting new completions.
            prev_char = self.view.substr(self.view.sel()[0].begin() - 1)
            if prev_char.isspace():
                if last_text_command == "insert_best_completion":
                    self.view.run_command("undo")

            self.state = CompletionState.APPLYING
            self.view.run_command("hide_auto_complete")
            self.run_auto_complete()
        elif self.state == CompletionState.CANCELLING:
            self.state = CompletionState.IDLE
            if self.next_request:
                prefix, locations = self.next_request
                self.do_request(prefix, locations)
        else:
            debug('Got unexpected response while in state {}'.format(self.state))

    def handle_error(self, error: dict) -> None:
        sublime.status_message('Completion error: ' + str(error.get('message')))
        self.state = CompletionState.IDLE

    def run_auto_complete(self) -> None:
        self.view.run_command(
            "auto_complete", {
                'disable_auto_insert': True,
                'api_completions_only': settings.only_show_lsp_completions,
                'next_completion_if_showing': False
            })