import typing

import matplotlib.pyplot as plt
import numpy as np
from numpy.polynomial.polynomial import Polynomial


def pi_n(alpha: float, n: int):
    """
    Вычисляет значение предельной вероятности системы при n заявках.
    Значение n <= числа обслуживающих каналов в системе.
    :param alpha: Коэффициент загрузки системы.
    :param n: Количество заявок в системе.
    :return: Значение предельной вероятности системы при n заявках.
    """
    return alpha ** n / (np.math.factorial(n) * np.sum(
        [alpha ** k / np.math.factorial(k) for k in range(n + 1)]) + alpha ** (n + 1) / (
                                 n - alpha))


def calc_ey(mu: float, lam: float, n: int):
    """
    Вычилсяет математическое ожидание заявки от поступления ее в систему
    до начала обслуживания одним из каналов.
    :param mu: Интенсивность обслуживания заявок одним каналом узла.
    :param lam: Интенсивность входящего в узел потока из одиночных пассажиров.
    :param n: Количество каналов обслуживания в узле.
    :return: Значение математического ожидания времени ожидания заявки в очереди.
    """
    alpha = lam / mu
    return n * pi_n(alpha, n) / (mu * (n - alpha) ** 2)


def fit_ey(mu: float, lam: float, n_min: int, n_max: int, p_pow=4, plot=False):
    """
    Аппроксимирует математическое ожидание времени ожидания заявки в очереди
    полиномом заданной степени.
    :param mu: Интенсивность обслуживания заявок одним каналом узла.
    :param lam: Интенсивность входящего в узел потока из одиночных пассажиров.
    :param n_min: Минимально возможное число каналов обслуживания в узле.
    :param n_max: Максимально возможное число каналов обслуживания в узле.
    :param p_pow: Степень полинома для аппроксимации.
    :param plot: Визуализировать приближение полиномом.
    :return: Массив из коэффициентов полинома.
    """
    n = np.arange(n_min, n_max + 1)
    ey = np.array([calc_ey(mu, lam, n_) for n_ in n])
    if len(n) < p_pow:
        p_pow = len(n) - 1
    if p_pow:
        res = Polynomial.fit(n, np.log(ey), p_pow, full=True)
        coef = res[0].convert().coef[::-1]
        rss = res[1][0][0]
    else:
        coef = np.log(ey)
        rss = 0.

    if plot:
        x = np.linspace(n_min - 1, n_max + 1, 100)
        f = np.poly1d(coef)

        plt.figure(figsize=(13, 8))
        plt.scatter(n, np.log(ey), s=55)
        plt.plot(x, f(x), label=f'Аппроксимирующая кривая (полином), RSS = {np.round(rss, 2)}')
        plt.title(
            'Логарифм мат. ожидания времени ожидания начала обслуживания в зависимости от числа обслуживающих каналов в узле')
        plt.xlabel('Количество обслуживающих каналов в узле')
        plt.ylabel('Логарифм мат. ожидания времени ожидания начала обслуживания')
        plt.legend()
        plt.grid()
        plt.show()

        rss = np.sum(np.square(ey - np.array([np.exp(f(n_)) for n_ in n])))
        plt.figure(figsize=(13, 8))
        plt.scatter(n, ey, s=55)
        plt.plot(x, np.exp(f(x)), label=f'Аппроксимирующая кривая, RSS = {np.round(rss, 2)}')
        plt.title(
            'Мат. ожидание времени ожидания начала обслуживания в зависимости от числа обслуживающих каналов в узле')
        plt.xlabel('Количество обслуживающих каналов в узле')
        plt.ylabel('Мат. ожидание времени ожидания начала обслуживания')
        plt.legend()
        plt.grid()
        plt.show()

    return coef


def calc_time(x: list, p: list):
    """
    Рассчет суммарного среднего времени ожидания.
    :param x: Количество каналов обслуживания в каждом узле.
    :param p: Массив из коэффициентов полинома,
    описывающего логарифм среднего времени ожидания в каждом узле.
    :return: Суммарное среднее время ожидания.
    """
    res = 0
    for x_, p_ in zip(x, p):
        f = np.poly1d(p_)
        res += np.exp(f(x_))
    return res


def constraint_cost(x: np.ndarray, p: list):
    """
    Ограничение для затрат на обслуживающий персонал.
    Должно быть >= 0.
    :param x: Массив. Количество каналов обслуживания в каждом узле.
    :param p[0]: Максимальное значение затрат на каналы обслуживания.
    :param p[1]: Массив. Стоимость каждого канала обслуживания.
    :return: Значение ограничения.
    """
    return np.array([p[0] - sum(x * p[1])])


def calc_cost(x: np.ndarray, p: np.ndarray):
    """
    Рассчет затрат на обслуживающий персонал.
    :param x: Количество каналов обслуживания в каждом узле.
    :param p: Массив. Стоимость каждого канала обслуживания.
    :return: Значение затрат.
    """
    return sum(x * p)


def constraint_time(x: list, p: list):
    """
    Ограничение для суммарного среднего времени ожидания. Должно быть >= 0.
    :param x: Количество каналов обслуживания в каждом узле.
    :param p[0]: Максимальное суммарное время ожидания пассажира в очереди.
    :param p[1]: Массив из коэффициентов полинома,
    описывающего логарифм среднего времени ожидания в каждом узле.
    :return: Значение ограничения.
    """
    res = p[0]
    for x_, p_ in zip(x, p[1]):
        f = np.poly1d(p_)
        res -= np.exp(f(x_))
    return res


class ConstraintTime:
    def __init__(self, t_max: float, coefs: np.ndarray):
        """
        Ограничение для суммарного среднего времени ожидания. Должно быть >= 0.
        :param t_max: Максимальное суммарное время ожидания пассажира в очереди.
        :param coefs: Массив из коэффициентов полинома,
        описывающего логарифм среднего времени ожидания в каждом узле.
        """
        self.t_max = t_max
        self.coefs = coefs

    def __call__(self, x: typing.Union[list, np.ndarray]):
        """
        :param x: Количество каналов обслуживания в каждом узле.
        описывающего логарифм среднего времени ожидания в каждом узле.
        :return: Значение ограничения.
        """
        res = self.t_max
        for x_, p_ in zip(x, self.coefs):
            f = np.poly1d(p_)
            res -= np.exp(f(x_))
        return res


def f_ey(n: list, mu: list, lam: float, p: list):
    """
    Вычилсяет суммарное математическое ожидание заявки
    в сети массового обслуживания.
    :param n: Массив. Количество каналов обслуживания в узлах сети.
    :param mu: Массив. Интенсивности обслуживания заявок узлами сети.
    :param lam: Интенсивность входящего в первый узел потока из одиночных пассажиров.
    :param p: Массив из вероятностей перехода в следующие после первого узлы сети.
    :return: Значение суммарного математического ожидания времени ожидания заявки в очередях сети.
    """
    l = np.insert(lam * np.cumprod(p), 0, lam)
    return sum([calc_ey(mu[i], l[i], n[i]) for i in range(len(n))])


def f_conv(n: list, mu: list, lam: float, p: list, n_min: list, n_max: list, s: list, w: list):
    """
    Рассчет взвешенной суммы суммарного времени ожидания и затрат на обслуживающий персонал.
    :param n: Массив. Количество каналов обслуживания в узлах сети.
    :param mu: Массив. Интенсивности обслуживания заявок узлами сети.
    :param lam: Интенсивность входящего в первый узел потока из одиночных пассажиров.
    :param p: Массив из вероятностей перехода в следующие после первого узлы сети.
    :param n_min: Массив. Минимальное число каналов в каждом узле.
    :param n_max: Массив. Максимальное число каналов в каждом узле.
    :param s: Массив. Стоимость каждого канала обслуживания.
    :param w: Массив. Коэффициенты свертки каждого критерия.
    :return: Значение взвешенной суммы критериев.
    """
    a1 = f_ey(n_max, mu, lam, p)
    a1_max = f_ey(n_min, mu, lam, p)
    a2 = calc_cost(n_min, s)
    a2_max = calc_cost(n_max, s)
    return w[0] * (f_ey(n, mu, lam, p) - a1) / (a1_max - a1) + w[1] * (
            calc_cost(n, s) - a2) / (a2_max - a2)


def f_ideal(n: list, mu: list, lam: float, p: list, n_min: list, n_max: list, s: list, w: list, m: str):
    """
    Рассчет расстояния до идеальной точки.
    :param n: Массив. Количество каналов обслуживания в узлах сети.
    :param mu: Массив. Интенсивности обслуживания заявок узлами сети.
    :param lam: Интенсивность входящего в первый узел потока из одиночных пассажиров.
    :param p: Массив из вероятностей перехода в следующие после первого узлы сети.
    :param n_min: Массив. Минимальное число каналов в каждом узле.
    :param n_max: Массив. Максимальное число каналов в каждом узле.
    :param s: Массив. Стоимость каждого канала обслуживания.
    :param w: Массив. Веса каждого критерия.
    :param m: Строка ('2-norm' или 'inf'). Задает норму для вычисления отклонения
    критерия от идеальной точки.
    :return: Расстояние до идеальной точки.
    """
    metrics = ['2-norm', 'inf']
    assert m in metrics
    a1 = f_ey(n_max, mu, lam, p)
    a1_max = f_ey(n_min, mu, lam, p)
    a2 = calc_cost(n_min, s)
    a2_max = calc_cost(n_max, s)
    if m == metrics[0]:
        return w[0] * ((f_ey(n, mu, lam, p) - a1) / (a1_max - a1)) ** 2 + w[1] * (
                (calc_cost(n, s) - a2) / (a2_max - a2)) ** 2
    else:
        return max(w[0] * abs((f_ey(n, mu, lam, p) - a1) / (a1_max - a1)), w[1] *
                   abs((calc_cost(n, s) - a2) / (a2_max - a2)))


def calc_conv(x: list, p: list):
    """
    Рассчет взвешенной суммы времени ожидания и затрат на обслуживающий персонал.
    :param x: Количество каналов обслуживания в каждом узле.
    :param p[0]: Массив. Коэффициенты свертки каждого критерия.
    :param p[1]: Массив из коэффициентов полинома,
    описывающего логарифм среднего времени ожидания в каждом узле.
    :param p[2]: Массив. Стоимость каждого канала обслуживания.
    :param p[3]: Массив. Минимальное число каналов в каждом узле.
    :param p[4]: Массив. Максимальное число каналов в каждом узле.
    :return: Значение взвешенной суммы критериев.
    """
    a1 = calc_time(p[4], p[1])
    a1_max = calc_time(p[3], p[1])
    a2 = calc_cost(p[3], p[2])
    a2_max = calc_cost(p[4], p[2])
    return p[0][0] * (calc_time(x, p[1]) - a1) / (a1_max - a1) + p[0][1] * (
            calc_cost(x, p[2]) - a2) / (a2_max - a2)


def calc_ideal(x: list, p: list):
    """
    Рассчет расстояния до идеальной точки.
    :param x: Количество каналов обслуживания в каждом узле.
    :param p[0]: Массив. Веса каждого критерия.
    :param p[1]: Массив из коэффициентов полинома,
    описывающего логарифм среднего времени ожидания в каждом узле.
    :param p[2]: Массив. Стоимость каждого канала обслуживания.
    :param p[3]: Массив. Минимальное число каналов в каждом узле.
    :param p[4]: Массив. Максимальное число каналов в каждом узле.
    :param p[5]: Строка ('2-norm' или 'inf'). Задает норму для вычисления отклонения
    критерия от идеальной точки.
    :return: Расстояние до идеальной точки.
    """
    metrics = ['2-norm', 'inf']
    assert p[5] in metrics
    a1 = calc_time(p[4], p[1])
    a1_max = calc_time(p[3], p[1])
    a2 = calc_cost(p[3], p[2])
    a2_max = calc_cost(p[4], p[2])
    if p[5] == metrics[0]:
        return p[0][0] * ((calc_time(x, p[1]) - a1) / (a1_max - a1)) ** 2 + p[0][1] * (
                (calc_cost(x, p[2]) - a2) / (a2_max - a2)) ** 2
    else:
        return max(p[0][0] * abs((calc_time(x, p[1]) - a1) / (a1_max - a1)), p[0][1] *
                   abs((calc_cost(x, p[2]) - a2) / (a2_max - a2)))
