# vim: fileencoding=utf-8
#
from ..variables.datavars import DependenceAPI, VariableWrapper, NamespaceNode
from ..variables.loader import Datavars
from collections import OrderedDict
from contextlib import contextmanager
from typing import Tuple, Union, Any


class ParameterError(Exception):
    ...


class ValidationError(ParameterError):
    ...


class CyclicValidationError(ValidationError):
    def __init__(self, *queue):
        self.queue = queue

    def __str__(self):
        return "Cyclic validation for parameters: {}".format(", ".join(
                                                              self.queue[:-1]))


class ParameterType:
    '''Общий класс типов параметров.'''
    descr = 'VALUE'

    def process_value(self, value, parameter):
        return value


class Integer(ParameterType):
    '''Класс типа целочисленных параметров.'''
    descr = 'INTEGER'

    def process_value(self, value, parameter) -> int:
        if isinstance(value, int):
            return value
        raise ValidationError(f"Can not assign '{type(value)}' value to"
                              " parameter of the int type")

    def __repr__(self):
        return '<Integer Parameter Type>'


class String(ParameterType):
    '''Класс типа строковых параметров.'''
    descr = 'STRING'

    def process_value(self, value, parameter) -> str:
        if isinstance(value, str):
            return value
        raise ValidationError(f"Can not assign '{type(value)}' value to"
                              " parameter of the string type")

    def __repr__(self):
        return '<String Parameter Type>'


class Bool(ParameterType):
    '''Класс типа строковых параметров.'''
    false_set = {'n', 'false', 'False', 'no'}
    true_set = {'y', 'true', 'True', 'yes'}
    descr = '[y/n]'

    def process_value(self, value, parameter) -> bool:
        if isinstance(value, bool):
            return value
        raise ValidationError(f"Can not assign '{type(value)}' value to"
                              " parameter of the bool type")

    def __repr__(self):
        return '<Bool Parameter Type>'


class Float(ParameterType):
    '''Класс типа вещественночисловых параметров.'''
    descr = 'FLOAT'

    def process_value(self, value, parameter) -> float:
        if isinstance(value, float):
            return value
        raise ValidationError(f"Can not assign '{type(value)}' value to"
                              " parameter of the float type")

    def __repr__(self):
        return '<Float Parameter Type>'


class Separator:
    '''Класс объекта сепаратора, возможно, его не будет'''
    def __repr__(self):
        return "# # #"


class Choice(ParameterType):
    '''Класс типа, представлющий собой любое значение из совокупности заданных.
    '''
    descr = 'CHOICES'

    def __init__(self, choices=OrderedDict(), editable=False,
                 multichoice=False):
        self.editable = editable
        self.multichoice = multichoice
        self.choices = choices

    def add_choice(self, choice, comment):
        self.choices.update({choice: comment})

    def process_value(self, value, parameter):
        if value in self.choices:
            return value
        else:
            raise ValidationError(f"value '{value}' is not available in"
                                  f" parameter '{parameter._name}' available"
                                  f" values: '{', '.join(self.choices)}'")

    def __repr__(self):
        return (f'<Choice Parameter Type: choices={", ".join(self.choices)},'
                f' editable={self.editable}, multichoice={self.multichoice}>')


class List(ParameterType):
    descr = 'LIST'

    def process_value(self, value, parameter):
        if isinstance(value, list):
            return value
        raise ValidationError(f"Can not assign '{type(value)}' value to"
                              " parameter of the list type")

    def __repr__(self):
        return ('<List Parameter Type>')


class TableValue:
    '''Класс значения таблицы.'''
    def __init__(self, key, fields, fill_function=lambda x: x):
        self._fields = fields
        self._values = OrderedDict()

        # Родительский параметр.
        self._parent = None

        # Ключ и его индекс в массиве.
        self.primary_key = key

        # Устанавливаем комментарии стобцам таблицы, по умолчанию то же, что и
        # название столбца.
        self._fields_comments = OrderedDict()
        for field in self._fields:
            self._fields_comments.update({field: field})

        # Функция для заполнения пустых полей таблицы.
        self.fill = fill_function

        # Функция для кастомизации уведомления об ошибке.
        def set_error(value: str, field: str, available: list) -> None:
            raise ParameterError(f"{field} '{value}' is not found.")
        self.set_error = set_error

    def set_comments(self, *comments):
        '''Метод для установки комментариев к полям таблицы.'''
        if len(comments) < len(self._fields):
            raise ParameterError("Not enough values to comment table fields.")
        elif len(comments) > len(self._fields):
            raise ParameterError("Too much values to comment table fields.")

        self._fields_comments = OrderedDict()
        for field, comment in zip(self._fields, comments):
            if comment is not None:
                self._fields_comments[field] = comment

    def change(self, *values):
        '''Метод для добавления новых или изменения существующих строк таблицы.
        '''
        if len(values) < len(self._fields):
            raise ParameterError("Not enough values to fill table row.")
        elif len(values) > len(self._fields):
            raise ParameterError("Too much values to fill table row.")

        # Проверяем типы устанавливаемых значений.
        validated_values = OrderedDict()
        for value, (field, field_type) in zip(values,
                                              self._fields.items()):
            if value is None or self._parent is None:
                validated_values.update({field: value})
            else:
                validated_values.update({field: field_type.process_value(
                                                                value,
                                                                self._parent)})

        current_key = validated_values[self.primary_key]
        if current_key in self._values:
            self._values[current_key] = validated_values
        elif self._parent is None or self._parent._parameter_type.expandable:
            self._values.update({current_key: validated_values})
        else:
            self.set_error(current_key,
                           self._fields_comments[self.primary_key],
                           list(self._values.keys()))

        self._values = self.fill(self._values)

    def check_values_types(self):
        '''Метод для проверки по типам всех значений таблицы.'''
        for row in self._values.values():
            for field, field_type in self._fields.items():
                if row[field] is not None:
                    row[field] = field_type.process_value(row[field],
                                                          self._parent)

    def get_for_var(self):
        '''Метод для преобразования данного описания таблицы в ту, которая
        приемлема для инициализации переменных.'''
        output = []
        for value in self._values.values():
            output.append(value)
        return output


class Table(ParameterType):
    '''Метод реализующий тип таблиц.'''
    descr = 'LIST'

    def __init__(self, expandable=False):
        self.expandable = expandable

    def process_value(self, value: TableValue, parameter):
        if isinstance(value, TableValue):
            # Если родитель не установлен -- значит параметр был только что
            # инициализирован.его, и проводим полную проверку
            # инициализированных значений таблицы.
            if value._parent is None:
                value._parent = parameter
                value.check_values_types()
            return value
        raise ValidationError(f"Can not assign '{type(value)}' value to"
                              " parameter of the table type")

    def __repr__(self):
        return (f'<Table Parameter Type: expandable={self.expandable}>')


class File(ParameterType):
    pass


class Password(ParameterType):
    '''Класс типа строковых параметров.'''
    descr = 'PASSWORD'

    def __init__(self, param=False):
        # TODO разобраться с параметрами, необходимыми для параметров типа
        # Password.
        self.param = param

    def process_value(self, value, parameter) -> str:
        if isinstance(value, str):
            return value
        raise ValidationError(f"Can not assign '{type(value)}' value to"
                              " parameter of the string type")

    def __repr__(self):
        return '<String Parameter Type>'


class Description:
    '''Класс, содержащий описание параметра и способ его отображения.'''
    # словарь со способами отображения параметра в графическом интерфейсе.
    representations = {}

    def __init__(self, short='', full='', usage=''):
        self.short = short
        self.full = full
        self.usage = usage

        self._gui_repr = None

    def initialize(self, parameter_name, parameter_type, shortname=None):
        '''Метод для инициализации представления по указанной пользователем
        информации, типу параметра и его имени.'''
        if not self.short:
            self.short = parameter_name
        if not self.full:
            self.full = self.short
        if not self.usage:
            if shortname:
                self.usage = f"-{shortname} {parameter_type.descr},  "
            self.usage = self.usage +\
                f"--{parameter_name} {parameter_type.descr}"

        self._gui_repr = self._get_gui_repr(parameter_type)

    def _get_gui_repr(self, parameter_type):
        '''Метод для получения по типу параметра способа его представления.'''
        # TODO реализовать его, когда удастся выделить совокупность доступных
        # способов представления.
        return None

    @property
    def gui_repr(self):
        return self._gui_repr


class BaseParameter:
    '''Класс базовый класс всех параметров.'''
    default_type = ParameterType

    def __init__(self, name, group, desctiption: Description,
                 shortname=None, argv=None):
        self._group = group
        self._name = name
        self._shortname = shortname
        self._argv = argv

        # Ссылка на экземпляр контейнера параметров.
        self.container = None

        self._value = None
        # Допустимые значения параметра, если таковые имеются.
        # self.choices = OrderedDict()
        # Комментарии, если таковой необходим.
        self.comment = None

        # Флаг, указывающий на то, что значение параметра было изменено
        # установлено пользователем с помощью метода set.
        self._set_by_user = False
        self._validated = False

        if hasattr(self, 'type'):
            self._parameter_type = self.type
        else:
            self._parameter_type = self.default_type()

        # Добавляем описание и инициализируем его.
        self._description = desctiption
        self._description.initialize(self._name, self._parameter_type,
                                     shortname=self._shortname)

        # Комментарий, наличие которого указывает на неактивность параметра.
        self.disactivity_comment = None

        # Коллекции текущих подписок, аргументов, из которых эти подписки
        # формируются и подписчиков.
        self._subscriptions = {}
        self._args = []
        self._subscribers = []

        # Флаги для проверки того необходимо ли проводить поиск подписок или
        # подписчиков.
        self._args_is_found = False
        self._sets_is_found = False

        # Метод для указания того, что в данный момент осуществляется расчет
        # значения параметра.
        self._calculating = False
        self._validation = False

    @property
    def choices(self):
        if isinstance(self._parameter_type, Choice):
            return self._parameter_type.choices

    @property
    def disactivity(self):
        return bool(self.disactivity_comment)

    def validate(self, container, datavars, value) -> None:
        '''Метод для проверки корректности параметра. Переопределяется при
        создании нового типа параметра.'''
        return None

    def validate_value(self, value):
        '''Метод для запуска валидации параметра с использованием
        переопределенного метода валидации.'''
        if self._validation:
            raise CyclicValidationError(self._name)
        with self._start_validation():
            try:
                self.validate(self.container, self.container.datavars, value)
            except CyclicValidationError as error:
                raise CyclicValidationError(self._name, *error.queue)

    @contextmanager
    def _start_validation(self):
        '''Контекстный менеджер предназначеный для переключения параметра в
        режим валидации.'''
        try:
            self._validation = True
            yield self
        finally:
            self._validation = False

    def set(self, value) -> None:
        '''Метод для установки значения параметра.'''
        if isinstance(self._parameter_type, Table):
            print('VALUE:', self.value)
            self._value.change(*value)
        else:
            self._value = value

        self._set_by_user = True
        # Назначаем новые значения подписанным переменным.
        self._set_variables()

    @property
    def value(self):
        '''Метод для получения значения параметра. Выдается или дефолтное или
        установленное пользователем.'''
        if not self._set_by_user:
            if self._value is None and not self._calculating:
                self._value = self.update_value()
        return self._value

    def check_value_type(self, value):
        '''Метод для запуска проверки значения по типу параметра.'''
        return self._parameter_type.process_value(value, self)

    def bind(self, *variables) -> None:
        '''Метод для настройки взаимосвязи параметра с переменными.'''
        self._args = variables
        return self

    def bind_method(self, *variables):
        '''Метод переопределяемый для создания нового типа параметра. Должен
        возвращать вычисленное значение и активность параметра None -- активен,
        comment -- дизактивирован.'''
        if variables:
            return variables[0].value, None
        else:
            return None, None

    def to_set(self, *variables):
        '''Метод для добавления переменных, которым будет установлено значение
        параметра.'''
        self._subscribers = variables
        return self

    def _set_variables(self):
        '''Метод для установки значения параметра переменным-подписчикам.'''
        # Только если значение установлено пользователем.
        if self._set_by_user:
            if not self._sets_is_found:
                self._subscribers = self.find_variables(self._subscribers)
                self._sets_is_found = True
            for subscriber in self._subscribers:
                if isinstance(self._parameter_type, Table):
                    subscriber.set(self.value.get_for_var())
                else:
                    subscriber.set(self.value)

    def _invalidate(self):
        '''Метод, через который переменные сообщают о необходимости пересчитать
        значение параметра с дефолтным значением.'''
        value = self.update_value()
        if not self._set_by_user:
            self._value = value
            self._validated = False

    def update_value(self):
        '''Метод для получения дефолтного значения.'''
        if not self._args_is_found:
            self._args = self.find_variables(self._args)
            self._args_is_found = True

        with self._start_calculate():
            value, subscriptions = self._calculate_binding()

            # Обновляем подписки. Сначала убираем лишние.
            for subscription in self._subscriptions:
                if subscription not in subscriptions:
                    subscription.subscribers.remove(self)

            # Теперь добавляем новые.
            for subscription in subscriptions:
                subscription.subscribers.add(self)
            self._subscriptions = subscriptions

        return self._parameter_type.process_value(value, self)

    def _calculate_binding(self):
        '''Метод для расчета значения параметра по умолчанию, используя
        заданные переменные.'''
        subscriptions = set()
        args = tuple(VariableWrapper(arg, subscriptions) for arg in self._args)
        try:
            value, self.disactivity_comment = self.bind_method(*args)
            return value, subscriptions
        except Exception as error:
            raise ParameterError('can not calculate using dependencies: {}'
                                 ' reason: {}'.format(', '.join(
                                                   [subscription.get_fullname()
                                                    for subscription
                                                    in self._args]),
                                                       str(error)))

    @contextmanager
    def _start_calculate(self):
        '''Менеджер контекста устанавливающий флаг, указывающий, что данная
        переменная в состоянии расчета.'''
        try:
            self._calculating = True
            yield self
        finally:
            self._calculating = False

    def find_variables(self, variables):
        '''Метод для поиска переменных, по их названию. Обходит список
        переменных и заменяет все строки с запросами в списке на результат
        поиска по запросу.'''
        output = []
        for variable in variables:
            if isinstance(variable, str):
                variable = DependenceAPI.find_variable(variable,
                                                       self.container.datavars)
            output.append(variable)
        return output

    def __repr__(self):
        description = self._description.short or self._description.full
        return (f"<Parameter: '{self._name}' = "
                f"{self._value if self._value else 'NULL'}: "
                f"{description}>")


class GroupWrapper:
    '''Класс обертки параметров группы, который возможно будет использоваться
    для организации работы графического интерфейса.'''
    def __init__(self, group, container):
        self.values = OrderedDict()
        self.container = container

    def set_group(self):
        '''Метод для установки значений параметров, осносящихся к группе.'''
        self.container.set_parameters(self.values)


class Parameters:
    '''Класс контейнера параметров.'''
    def __init__(self, datavars: Datavars, check_order=[]):
        self._datavars = datavars
        self._parameters = OrderedDict()
        self._validation_dict = OrderedDict()
        self._order = check_order

        # Флаг указывающий на то, что в данный момент идет валидация параметров
        self._validation = False
        # Занятые имена параметров.
        self._names = set()
        # Список позиционных аргументов.
        self._argvs = []

        # Словарь функций для взаимодействия графического клиента с группами
        # параметров.
        self._gui_helpers = {}

    def add(self, *parameters: Tuple[BaseParameter]):
        '''Метод для добавления некоторой совокупности параметров.'''
        for parameter in parameters:
            self.add_parameter(parameter)

    def set_order(self, *parameters):
        '''Метод для установки порядка проверки параметров.'''
        self._order = []
        for parameter in parameters:
            if isinstance(parameter, str):
                parameter = self[parameter]
            self._order.append(parameter)

        for parameter in self:
            if parameter not in self._order:
                self._order.append(parameter)

    def add_parameter(self, parameter: BaseParameter) -> None:
        '''Метод для добавления параметров в контейнер.'''
        if parameter._name in self._names:
            raise ParameterError(f"Can not add parameter '{parameter._name}'."
                                 " Such name has already been added. ")
        elif parameter._shortname and parameter._shortname in self._names:
            raise ParameterError(f"Can not add parameter '{parameter._name}'."
                                 " Such name has already been added. ")
        elif (parameter._argv is not None and
              len(self._argvs) > parameter._argv and
              self._argvs[parameter._argv]):
            raise ParameterError(f"Can not add positional parameter "
                                 f"'{parameter._name}'. Position"
                                 f"'{parameter._argv}' is already taken.")

        parameter.container = self
        if parameter._group in self._parameters:
            self._parameters[parameter._group].append(parameter)
        else:
            self._parameters[parameter._group] = [parameter]

        self._names.add(parameter._name)
        if parameter._shortname:
            self._names.add(parameter._shortname)
        if parameter._argv is not None:
            # Если нужно, расширяем список позиций.
            if len(self._argvs) <= parameter._argv:
                while(len(self._argvs) <= parameter._argv):
                    self._argvs.append(None)
            self._argvs[parameter._argv] = parameter

        self._order.append(parameter)
        parameter.update_value()

    def set_parameters(self, parameters: OrderedDict) -> None:
        '''Метод для установки значений некоторого числа параметров их
        проверки.'''
        # Сначала проверяем все значения по типам и составляем словарь
        # валидации.
        for parameter_name, value in parameters.items():
            parameter = self[parameter_name]
            if not parameter.disactivity_comment:
                self._validation_dict[parameter] =\
                    parameter._parameter_type.process_value(value,
                                                            parameter)
                parameter._validated = False

        # Теперь запускаем переопределенный метод для проверки параметров.
        with self._run_validation():
            self.validate_parameters(self._validation_dict)

    def validate_parameters(self, parameters: OrderedDict):
        '''Метод для запуска валидации параметров.'''
        for parameter in self._order:
            if parameter in self._validation_dict:
                if (not parameter._validated
                   and not parameter.disactivity_comment):
                    parameter.validate_value(self._validation_dict[parameter])
                    parameter._validated = True
                    parameter.set(self._validation_dict[parameter])

    def validate_all(self):
        for parameter in self._order:
            if (not parameter._validated and
               not parameter.disactivity_comment):
                parameter.validate_value(self._validation_dict[parameter])
                parameter._validated = True
                parameter.set(self._validation_dict[parameter])

    @contextmanager
    def _run_validation(self):
        '''Контекстный менеджер предназначенный для перевода контейнера
        параметров в режим валидации.'''
        try:
            self._validation = True
            yield self
        finally:
            self._validation = False
            self._validation_dict = OrderedDict()

    def get_group_parameters(self, group_name: str):
        '''Метод для получения списка параметров, относящихся к указанной
        группе.'''
        return self._parameters[group_name]

    def get_descriptions(self) -> dict:
        '''Метод для получения словаря с описанием параметров.'''
        output = OrderedDict()
        for group, parameters in self._parameters.items():
            for parameter in parameters:
                usage = '  ' + parameter._description.usage
                full = parameter._description.full

                if len(usage) > 23:
                    usage = usage + '\n'
                    full = ' ' * 24 + full
                elif len(usage) == 23:
                    usage = usage + ' '
                else:
                    usage = usage.ljust(23) + ' '

                parameter_description = (f"{usage}{full}")
                if group in output:
                    output[group].append(parameter_description)
                else:
                    output[group] = [parameter_description]
        return output

    @property
    def datavars(self) -> Union[Datavars, NamespaceNode]:
        return self._datavars

    def __getitem__(self, name: str) -> Any:
        for group, parameters in self._parameters.items():
            for parameter in parameters:
                if parameter._name == name or parameter._shortname == name:
                    if self._validation and not parameter._validated:
                        # В режиме валидации параметров, при попытке получить
                        # параметр сначала проверяем необходимость его проверки
                        # И если нужно проверяем его.
                        parameter_value =\
                            self._validation_dict.get(parameter, None) or\
                            parameter._value
                        parameter.validate_value(parameter_value)
                        parameter._validated = True
                        return parameter
                    else:
                        return parameter
        raise ParameterError(f"No such parameter '{name}'.")

    def __iter__(self):
        for parameters in self._parameters.values():
            for parameter in parameters:
                yield parameter
