from datetime import datetime, timedelta
from IPython.display import display
from math import factorial
from matplotlib import pyplot as plt

import io
import numpy as np
import pandas as pd

Σ = sum

%matplotlib inline

"""
Explanation: Sistemas de filas
Após uma contagem de, pelo menos, uma hora em um sistema que apresente fila de espera, preferencialmente com mais de um servidor, o aluno deverá apresentar respostas às perguntas que serão apresentadas a seguir.
Terminologias

N = O número de clientes no sistema de filas
P_n = Probabilidade de que exatamente n clientes estejam no sistema de fila
S = número de servidores no sistema de filas (canais de serviço paralelos)
l = taxa média de chegada de clientes (número esperado de chegadas por tempo unitário
μ = taxa média de serviço de serviço para todo o sistema (número esperado de clientes concluindo o serviço por unidade de tempo)
L = número esperado de clientes no sistema
L_q = número esperado de clientes na fila
W = Tempo de espera no sistema para cada cliente em particular (inclui tempo de serviço)
W_q = Tempo de espera na fila para cada cliente em particular 
1/l = tempo esperado entre as chegadas
1/μ = tempo de serviço esperado
End of explanation
"""


def timetable(a, b):
    return b + timedelta(minutes=int(a))
    
v_timetable = np.vectorize(timetable)

def sampling_generator(
    size: int=20, initial_datetime: datetime=None
) -> np.array:
    """
    
    """
    if initial_datetime is None:
        initial_datetime = datetime.now()
    
    return v_timetable(
        np.random.randint(0, 60, size=size),
        datetime.now()
    )

def mean_diff_time(se_time: pd.Series) -> float:
    """
    
    """
    n = se_time.shape[0]
    l_diff = [0] * (n-1)
    
    for i in range(1, n):
        j = i-1
        l_diff[j] = (se_time[i] - se_time[j]).seconds/60
    
    return np.mean(l_diff)

initial_datetime = datetime.now()

df_chegada = pd.DataFrame({
    'entrada': sampling_generator(
        initial_datetime=initial_datetime
    )
}).sort_values(by='entrada').reset_index(drop=True)

df_saida = pd.DataFrame({
    'saída': sampling_generator(
        initial_datetime=initial_datetime
    )
}).sort_values(by='saída').reset_index(drop=True)

"""
fig, ax = plt.subplots(1, 2)
df_chegada.hist(ax=ax[0])

df_saida.hist(ax=ax[1])
plt.show()
"""
print('tables head')
display(df_chegada.head())
display(df_saida.head())

"""
Explanation: Dados
End of explanation
"""


initial_datetime.strftime('%d de %b de %Y - %l:%M%p')

"""
Explanation: a) Descrição do sistema de filas, local, data e horários da coleta de dados:
Fila do tipo: FIFO
Local: loja Plazafone
Início da coleta:
End of explanation
"""


S = 3
print('S:', S)

"""
Explanation: b) Número de servidores atendendo = S:
End of explanation
"""


μ_chegada = mean_diff_time(df_chegada['entrada'])
# fixando los valores para teste
μ_chegada = 3.05263157895
print('μ_chegada:', μ_chegada, 'mins')

"""
Explanation: c) O tempo médio entre as chegadas à fila ( um cliente a cada 5 min, por ex.):
End of explanation
"""


λ = 60/μ_chegada
print('λ:', λ)

"""
Explanation: d) A taxa de chegada de usuários no sistema de filas – λ ( clientes por unidade de tempo):
$\lambda = \frac{60 min}{\mu_chegada} (clientes/hora)$
End of explanation
"""


μ_atendimento = mean_diff_time(df_saida['saída'])
# fixando valores para teste
μ_atendimento = 2.84210526316
print('μ_atendimento:', μ_atendimento, 'mins')

"""
Explanation: e) O tempo médio de atendimento de cada cliente ( 20 min, por ex.):
End of explanation
"""


μ = 60/(μ_atendimento*S)
print('μ:', μ, 'clientes/hora')

"""
Explanation: f) A taxa de atendimento dos usuários – μ ( clientes por unidade de tempo, por servidor:
$\mu = \frac{60 min}{\mu_atendimento} (clientes/hora)$
Se existir mais que um servidor, a taxa de atendimento do sistema será multiplicada pelo número de servidores:
End of explanation
"""


ρ = (λ/(S*μ))*100
print('ρ:', ρ, '%')

"""
Explanation: g) A ocupação do sistema(em porcentagem);
$\rho = \frac{\lambda}{S*\mu} * 100 (\%)$
End of explanation
"""


P_0 = (
    Σ([(((λ/μ)**n)/(factorial(n))) for n in range(S)]) +
    (((λ/μ)**S)/(factorial(S) * (1-(λ/(S*μ)))))
)**(-1)

print('P_0:', P_0)

"""
Explanation: h) A probabilidade de o sistema estar vazio:
$$
P_0 = [
    \sum\limits_{n=0}^{S-1} (\frac{(\lambda / \mu)^n}{n!}) + 
    \frac{(\lambda/\mu)^S}{S!(1-(\lambda/(S.\mu)))}
]^{-1}
$$
End of explanation
"""


P_ocupados = (((λ/μ)**S)/(factorial(S)*(1-(λ/(S*μ))))) * P_0
print('P_ocupados:', P_ocupados)

"""
Explanation: i) A probabilidade de todos os servidores estarem ocupados:
$
P_{ocupados} = P(n \leq S) = \frac{(\lambda/\mu)^S}{S!(1-\lambda/(S.\mu))}.P_0
$
End of explanation
"""


L_q = ((((λ/μ)**S)*λ*μ*S)/(factorial(S)*((μ*S-λ)**2)))*P_0
print('L_q:', L_q, 'usuários na fila')

"""
Explanation: k) O número esperado de usuários na fila:
$
L_q = \frac{(\lambda/\mu)^S . \lambda . \mu . S}{S!(\mu . S - \lambda)^2} . P_0
$
End of explanation
"""


L = L_q + (λ/μ)
print('L:', L, 'usuários no sistema')

"""
Explanation: j) O número esperado de usuários no sistema:
$
L = L_q + \frac{\lambda}{\mu}
$
End of explanation
"""


W_q = L_q/λ
print('W_q:', W_q, 'horas')

"""
Explanation: l) O tempo médio dos usuários na fila:
$
W_q = \frac{L_q}{\lambda}
$
End of explanation
"""


W = L/λ
print('W:', W, 'horas')

"""
Explanation: m) O tempo provável dos usuários no sistema:
$
W = \frac{L}{\lambda}
$
End of explanation
"""


p_dobro = ((λ*2)/(S*μ))
print('p_dobro:', p_dobro*100, '%')

if p_dobro < 1:
    print('Ok: o sistema atinge a estabilidade')
else: 
    print('O sistema não atinge a estabilidade')

"""
Explanation: n) Se a taxa de chamadas duplicasse, o que aconteceria com o sistema? E quais as providências que deveriam ser tomadas.
End of explanation
"""


# dados de teste providos pelo professor
t_S = 3
t_λ = 19.655  # Clientes/hora
t_μ = 7.037  # Clientes/hora
t_ρ = 0.93  # %
t_P_0 = 0.016570  # 1,657 % - Probabilidade 0 clientes
# Prob S clientes no sistema 0.060176 -> 6,018 %
t_P_ocupados = 0.87251  # 87,251 %
t_L_q = 11.78  # clientes
t_L = 14.57  # clientes - Número médio clientes no sistema
t_W_q = 0.599  # horas - Tempo médio de espera na fila
t_W = 0.741  # horas - Tempo médio de espera sistema

np.testing.assert_approx_equal(S, t_S)
np.testing.assert_approx_equal(λ, t_λ, 3)
np.testing.assert_approx_equal(μ, t_μ, 3)
np.testing.assert_approx_equal(ρ, t_ρ*100, 1)
np.testing.assert_approx_equal(P_0, t_P_0, 3)
np.testing.assert_approx_equal(P_ocupados, t_P_ocupados, 3)
np.testing.assert_approx_equal(L_q, t_L_q, 3)
np.testing.assert_approx_equal(L, t_L, 3)
np.testing.assert_approx_equal(W_q, W_q, 3)
np.testing.assert_approx_equal(W, t_W, 3)

"""
Explanation: Antes de pensar em aumentar a quantidade de servidores, deve ser feito um estudo para identificar se é possível otimizar o tempo de atendimento que pode estar sendo prejudicado por algum procedimento ineficiente ou por lentidão dos rescursos computacionais ou de falta de habilidade do servidor. Depois de haver otimização dos recursos, caso não for suficiente, deverá ser avaliada a opção de aumentar o número de servidores nos horários críticos ou apresentar uma proposta para facilitar o atendimento, como pré-atendimento online ou alguma facilidade extra em horários de pouco fluxo de entrada de clientes
Teste
End of explanation
"""


