from typing import Union, Callable, List
from ..scripts.scripts import Script
from ..parameters.parameters import BaseParameter, Parameters
from ..variables.datavars import NamespaceNode, DependenceAPI,\
                                         VariableNotFoundError
from ..variables.loader import Datavars
from ..utils.io_module import IOModule


class CommandDescriptionError(Exception):
    '''Исключение кидаемое при наличии ошибок во время создания объекта
    команды.'''
    def __init__(self, message: str, command_id: str = '', title: str = ''):
        self.message: str = message
        self.command_id: str = command_id
        self.command_title: str = title

    def __str__(self) -> str:
        if self.command_title:
            return (f'can not create command {self.command_title}:'
                    f' {self.message}')
        if self.command_id:
            return f'can not create command {self.command_id}: {self.message}'
        return f'can not create command: {self.message}'


class CommandInitializationError(Exception):
    '''Исключение кидаемое при наличии ошибок во время инициализации.'''
    def __init__(self, message: str, title: str):
        self.message: str = message
        self.command_title: str = title

    def __str__(self) -> str:
        if self.command_title:
            return (f'can not initialize command {self.command_title}:'
                    f' {self.message}')


class Command:
    '''Класс описания команды, предназначен для хранения информации о команде
    и создания по данному описанию объекта лаунчера команды.'''
    def __init__(self, command_id: str = '',
                 category: str = '',
                 title: str = '',
                 script: Union[Callable, Script, None] = None,
                 args: Union[tuple, list] = tuple(),
                 parameters: Union[List[BaseParameter], None] = None,
                 namespace: Union[str, NamespaceNode, None] = None,
                 command: str = '',
                 gui: bool = False,
                 icon: Union[str, List[str]] = '',
                 setvars: dict = {},
                 rights: List[str] = []):
        # Идентификатор команды обязателен.
        if not command_id:
            raise CommandDescriptionError('command ID is not set')
        self.__id: str = command_id

        # Если собственно команда не была указана, получаем ее из ID.
        if command:
            self.__command: str = command
        else:
            self.__command: str = f'cl_{self.__id}'

        # Название команды.
        if not title:
            raise CommandDescriptionError("title is not set",
                                          command_id=command_id)
        self.__title: str = title

        # Категория, к которой относится команда.
        if not category:
            raise CommandDescriptionError("category is not set",
                                          command_title=title)
        self.__category: str = category

        # Пространство имен относительно которого, будет выполняться скрипт.
        self.__namespace: Union[str, NamespaceNode, None] = namespace

        # Параметры, указываемые при вызове этой команды.
        if parameters is None:
            raise CommandDescriptionError("parameters is not set",
                                          command_title=title)
        self.__parameters: List[BaseParameter] = parameters

        # Скрипт выполняемый при вызове этой команды.
        if not script:
            raise CommandDescriptionError("script is not set",
                                          command_title=title)
        self.__script: Script = script

        # Аргументы, с которыми будет запущен скрипт. Пока только статичные
        # значения.
        self.__args = args

        # Параметр указывающий, должена ли данная команда отображаться в
        # графическом интерфейсе.
        self.__gui: bool = gui

        # Устанавливаем название иконки.
        if not icon and self.__gui:
            raise CommandDescriptionError("icon is not set",
                                          command_title=title)
        self.__icon: Union[str, List[str]] = icon

        # Словарь с переменными и значениями, которые нужно им присвоить.
        self.__setvars: dict = setvars

        # Права, необходимые данной команде.
        # TODO разобраться с тем, как использовать.
        if not rights:
            raise CommandDescriptionError("rights is not set",
                                          title=title)
        self.__rights = rights

    def make_runner(self, datavars: Union[Datavars, NamespaceNode],
                    output: IOModule):
        '''Метод создающий на основе данного описания команды экземпляр раннера.
        '''
        return CommandRunner(self, datavars, output)

    # Интерфейс иммутабельного объекта описания команды.
    @property
    def id(self) -> str:
        return self.__id

    @property
    def command(self) -> str:
        return self.__command

    @property
    def title(self) -> str:
        return self.__title

    @property
    def category(self) -> str:
        return self.__category

    @property
    def namespace(self) -> str:
        return self.__namespace

    @property
    def parameters(self) -> str:
        return self.__parameters

    @property
    def script(self) -> Script:
        return self.__script

    @property
    def args(self) -> tuple:
        return self.__args

    @property
    def gui(self) -> bool:
        return self.__gui

    @property
    def icon(self) -> Union[str, List[str]]:
        return self.__icon

    @property
    def setvars(self) -> dict:
        return self.__setvars

    @property
    def rights(self) -> List[str]:
        return self.__rights

    def __repr__(self):
        return f"<Command: {self.__title}>"


class CommandRunner:
    def __init__(self, command: Command,
                 datavars: Union[Datavars, NamespaceNode],
                 output: IOModule):
        '''Класс инкапулирующий все данные о команде, а также необходимые для
        нее параметры, модуль ввода-вывода, переменные и прочее. Предназначен
        для конфигурации команды и запуска ее в воркере.'''
        self._command = command
        self._datavars = datavars
        self._output = output

        # Ищем пространство имен.
        if self._command.namespace is not None:
            if isinstance(self._command.namespace, str):
                self._namespace = self._find_namespace(
                                           self._command.namespace,
                                           datavars)
        # Инициализируем параметры.
        self._parameters = Parameters(self._datavars)
        self._parameters.add(*self._command.parameters)

        # Получаем запускатель скрипта.
        self._script_launcher = self._command.script.make_launcher(
                                                               output,
                                                               datavars,
                                                               self._namespace)
        # Устанавливаем переменные, если нужно.
        if self._command.setvars:
            self._set_variables(self._command.setvars,
                                datavars, self._namespace)

    def run_command(self):
        args = self._command.args
        self._script_launcher(*args)

    def set_parameters(self, values: dict):
        pass

    def get_parameters(self, group: Union[str, None] = None):
        '''Метод для установки параметров.'''
        pass

    def _find_namespace(self, namespace_path: str,
                        datavars: Union[Datavars, NamespaceNode]
                        ) -> NamespaceNode:
        '''Метод для поиска пространств имен.'''
        # TODO сделать где-нибудь единственный статический вариант.
        parts = namespace_path.split('.')
        namespace = datavars
        for part in parts:
            namespace = namespace[part]
        return namespace

    def _set_variables(self, setvars: dict,
                       datavars: Union[Datavars, NamespaceNode],
                       namespace: Union[NamespaceNode, None]) -> None:
        '''Метод для установки указанных значений, указанным переменным.'''
        force = False

        for varname, value in setvars.items():
            if varname.endswith('!'):
                varname = varname[:-1]
                force = True
            else:
                force = False

            try:
                variable = DependenceAPI.find_variable(
                                                   varname, datavars,
                                                   current_namespace=namespace)
            except VariableNotFoundError:
                self._output.set_error(f"Can not set variable '{varname}':"
                                       " variable does not exist.")

            if not variable.readonly or force:
                variable.set(value)
            else:
                self._output.set_error("Can not set readonly variable "
                                       f"'{variable.get_fullname}'.")
