import sublime
import sublime_plugin
from .core.edit import parse_workspace_edit
from .core.protocol import Diagnostic
from .core.protocol import Request, Point
from .core.registry import LspTextCommand
from .core.registry import sessions_for_view, client_from_session
from .core.settings import settings
from .core.typing import Any, List, Dict, Callable, Optional, Union, Tuple, Mapping, TypedDict
from .core.url import filename_to_uri
from .core.views import region_to_range
from .diagnostics import filter_by_point, view_diagnostics

CodeActionOrCommand = TypedDict('CodeActionOrCommand', {
    'title': str,
    'command': Union[dict, str],
    'edit': dict
}, total=False)
CodeActionsResponse = Optional[List[CodeActionOrCommand]]
CodeActionsByConfigName = Dict[str, List[CodeActionOrCommand]]


class CodeActionsAtLocation(object):

    def __init__(self, on_complete_handler: Callable[[CodeActionsByConfigName], None]) -> None:
        self._commands_by_config = {}  # type: CodeActionsByConfigName
        self._requested_configs = []  # type: List[str]
        self._on_complete_handler = on_complete_handler

    def collect(self, config_name: str) -> Callable[[CodeActionsResponse], None]:
        self._requested_configs.append(config_name)
        return lambda actions: self.store(config_name, actions)

    def store(self, config_name: str, actions: CodeActionsResponse) -> None:
        self._commands_by_config[config_name] = actions or []
        if len(self._requested_configs) == len(self._commands_by_config):
            self._on_complete_handler(self._commands_by_config)

    def deliver(self, recipient_handler: Callable[[CodeActionsByConfigName], None]) -> None:
        recipient_handler(self._commands_by_config)


class CodeActionsManager(object):
    """ Collects and caches code actions"""

    def __init__(self) -> None:
        self._requests = {}  # type: Dict[str, CodeActionsAtLocation]

    def request(self, view: sublime.View, point: int,
                actions_handler: Callable[[CodeActionsByConfigName], None]) -> None:
        current_location = self.get_location_key(view, point)
        # debug("requesting actions for {}".format(current_location))
        if current_location in self._requests:
            self._requests[current_location].deliver(actions_handler)
        else:
            self._requests.clear()
            self._requests[current_location] = request_code_actions(view, point, actions_handler)

    def get_location_key(self, view: sublime.View, point: int) -> str:
        return "{}#{}:{}".format(view.file_name(), view.change_count(), point)


actions_manager = CodeActionsManager()


def request_code_actions(view: sublime.View, point: int,
                         actions_handler: Callable[[CodeActionsByConfigName], None]) -> CodeActionsAtLocation:
    diagnostics_by_config = filter_by_point(view_diagnostics(view), Point(*view.rowcol(point)))
    return request_code_actions_with_diagnostics(view, diagnostics_by_config, actions_handler)


def request_code_actions_with_diagnostics(
    view: sublime.View,
    diagnostics_by_config: Dict[str, List[Diagnostic]],
    actions_handler: Callable[[CodeActionsByConfigName], None]
) -> CodeActionsAtLocation:
    actions_at_location = CodeActionsAtLocation(actions_handler)
    for session in sessions_for_view(view, 'codeActionProvider'):
        if session.config.name in diagnostics_by_config:
            point_diagnostics = diagnostics_by_config[session.config.name]
            file_name = view.file_name()
            relevant_range = point_diagnostics[0].range if point_diagnostics else region_to_range(
                view,
                view.sel()[0])
            if file_name:
                params = {
                    "textDocument": {
                        "uri": filename_to_uri(file_name)
                    },
                    "range": relevant_range.to_lsp(),
                    "context": {
                        "diagnostics": list(diagnostic.to_lsp() for diagnostic in point_diagnostics)
                    }
                }
                if session.client:
                    session.client.send_request(
                        Request.codeAction(params),
                        actions_at_location.collect(session.config.name))
    return actions_at_location


class LspCodeActionBulbListener(sublime_plugin.ViewEventListener):
    def __init__(self, view: sublime.View) -> None:
        super().__init__(view)
        self._stored_region = sublime.Region(-1, -1)
        self._actions = []  # type: List[CodeActionOrCommand]

    @classmethod
    def is_applicable(cls, _settings: dict) -> bool:
        if settings.show_code_actions_bulb:
            return True
        return False

    def on_selection_modified_async(self) -> None:
        self.hide_bulb()
        self.schedule_request()

    def schedule_request(self) -> None:
        try:
            current_region = self.view.sel()[0]
        except IndexError:
            return
        if self._stored_region != current_region:
            self._stored_region = current_region
            sublime.set_timeout_async(lambda: self.fire_request(current_region), 800)

    def fire_request(self, current_region: sublime.Region) -> None:
        if current_region == self._stored_region:
            self._actions = []
            actions_manager.request(self.view, current_region.begin(), self.handle_responses)

    def handle_responses(self, responses: CodeActionsByConfigName) -> None:
        for _, items in responses.items():
            self._actions.extend(items)
        if len(self._actions) > 0:
            self.show_bulb()

    def show_bulb(self) -> None:
        region = self.view.sel()[0]
        flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE
        self.view.add_regions('lsp_bulb', [region], 'markup.changed', 'Packages/LSP/icons/lightbulb.png', flags)

    def hide_bulb(self) -> None:
        self.view.erase_regions('lsp_bulb')


def is_command(command_or_code_action: CodeActionOrCommand) -> bool:
    command_field = command_or_code_action.get('command')
    return isinstance(command_field, str)


def execute_server_command(view: sublime.View, config_name: str, command: Mapping[str, Any]) -> None:
    session = next((session for session in sessions_for_view(view) if session.config.name == config_name), None)
    client = client_from_session(session)
    if client:
        client.send_request(
            Request.executeCommand(command),
            handle_command_response)


def handle_command_response(response: 'None') -> None:
    pass


def run_code_action_or_command(view: sublime.View, config_name: str,
                               command_or_code_action: CodeActionOrCommand) -> None:
    if is_command(command_or_code_action):
        execute_server_command(view, config_name, command_or_code_action)
    else:
        # CodeAction can have an edit and/or command.
        maybe_edit = command_or_code_action.get('edit')
        if maybe_edit:
            changes = parse_workspace_edit(maybe_edit)
            window = view.window()
            if window:
                window.run_command("lsp_apply_workspace_edit", {'changes': changes})
        maybe_command = command_or_code_action.get('command')
        if isinstance(maybe_command, dict):
            execute_server_command(view, config_name, maybe_command)


class LspCodeActionsCommand(LspTextCommand):
    def is_enabled(self) -> bool:
        return self.has_client_with_capability('codeActionProvider')

    def run(self, edit: sublime.Edit) -> None:
        self.commands = []  # type: List[Tuple[str, str, CodeActionOrCommand]]
        self.commands_by_config = {}  # type: CodeActionsByConfigName
        actions_manager.request(self.view, self.view.sel()[0].begin(), self.handle_responses)

    def combine_commands(self) -> 'List[Tuple[str, str, CodeActionOrCommand]]':
        results = []
        for config, commands in self.commands_by_config.items():
            for command in commands:
                results.append((config, command['title'], command))
        return results

    def handle_responses(self, responses: CodeActionsByConfigName) -> None:
        self.commands_by_config = responses
        self.commands = self.combine_commands()
        self.show_popup_menu()

    def show_popup_menu(self) -> None:
        if len(self.commands) > 0:
            self.view.show_popup_menu([command[1] for command in self.commands], self.handle_select)
        else:
            self.view.show_popup('No actions available', sublime.HIDE_ON_MOUSE_MOVE_AWAY)

    def handle_select(self, index: int) -> None:
        if index > -1:
            selected = self.commands[index]
            run_code_action_or_command(self.view, selected[0], selected[2])