from fastapi import APIRouter
from fastapi import status
from typing import List
from fastapi import Path
from fastapi import HTTPException
from models import Pedido
import uuid
from rotas import rota_produtos

pedidos_db = []

rota_pedidos = APIRouter()

@rota_pedidos.post("/",
                    status_code=status.HTTP_201_CREATED,
                    response_model=Pedido,
                    summary="Criar Pedido",
                    description="Cria pedido com os produtos.")
async def criar_pedido(pedido: Pedido):
    pedido_total = 0
    pedido_items = []
    for item in pedido.items:
        produto_id = item.produto_id
        quantity = item.quantity
        matching_produtos = [p for p in rota_produtos.produtos_db if p["id"] == produto_id]
        if not matching_produtos:
            raise HTTPException(status_code=400, detail=f"Produto ID {produto_id} não existe")
        produto = matching_produtos[0]
        produto_preco = produto["preco"]
        pedido_total += produto_preco * quantity
        pedido_items.append(item)
    pedido_id = uuid.uuid4()
    pedido.id = pedido_id
    pedido.items = pedido_items
    pedidos_db.append(pedido.dict())
    return pedido

@rota_pedidos.get("/",
                    response_model=List[Pedido],
                    summary="Consultar Pedidos",
                    description="Retorna uma lista de pedidos disponíveis ou uma lista vazia.")
async def list_pedidos():
    return pedidos_db

@rota_pedidos.get("/{pedido_id}",
                    response_model=Pedido,
                    summary="Consultar Pedido por ID",
                    description="Retorna um pedido por seu ID.")
async def get_pedido(pedido_id: uuid.UUID = Path(
                                                default=None,
                                                title='ID do tipo uui',
                                                description='Deve ser do tipo uuid')):
    matching_pedidos = [o for o in pedidos_db if o["id"] == pedido_id]
    if not matching_pedidos:
        raise HTTPException(status_code=404, detail=f"Pedido com ID {pedido_id} não encontrado")
    return matching_pedidos[0]

@rota_pedidos.delete("/{pedido_id}",
                    response_model=Pedido,
                    summary="Excluir Pedido",
                    description="Exclui um pedido por seu ID.")
async def delete_pedido(pedido_id: uuid.UUID):
    for idx, pedido in enumerate(pedidos_db):
        if pedido["id"] == pedido_id:
            deleted_pedido = pedidos_db.pop(idx)
            return deleted_pedido
    raise HTTPException(status_code=404, detail=f"Pedido com ID {pedido_id} não encontrado")


from helpers.redis import redis_connection

from threading import Thread
import datetime
import logging

logging.basicConfig(filename="activity_logs.txt", level=logging.INFO)

class Listener(Thread):
    def __init__(self):
        Thread.__init__(self)

    def run(self):
        pubsub = redis_connection.pubsub()
        pubsub.subscribe(["spam", "sign_out", "sign_in", "sign_up"])
        for item in pubsub.listen():
            if item["type"] == "message":
                log = f"({datetime.datetime.now()}) {item['data']}"
                logging.info(log)


import random
import math
from logging import StreamHandler, getLogger as realGetLogger, Formatter
import numpy as np
import pandas as pd
from colorama import Fore, Back, Style

###############################################################################
# Configure color stream handler
# https://gist.github.com/jonaprieto/a61d9cade3ba19487f98
###############################################################################
class ColourStreamHandler(StreamHandler):
    """ A colorized output StreamHandler """

    # Some basic colour scheme defaults
    colours = {
        'DEBUG': Fore.CYAN,
        'INFO': Fore.GREEN,
        'WARN': Fore.YELLOW,
        'WARNING': Fore.YELLOW,
        'ERROR': Fore.RED,
        'CRIT': Back.RED + Fore.WHITE,
        'CRITICAL': Back.RED + Fore.WHITE
    }

    def emit(self, record):
        try:
            message = self.format(record)
            self.stream.write(self.colours[record.levelname] + message +
                              Style.RESET_ALL)
            self.stream.write(getattr(self, 'terminator', '\n'))
            self.flush()
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)

###############################################################################
# configure logger
###############################################################################
def get_logger(name=None,
               fmt='[%(name)s::%(funcName)s] %(levelname)s %(message)s',
               level='INFO'):
    """ Get and initialize a colourised logging instance if the system supports
    it as defined by the log.has_colour
    :param name: Name of the logger
    :type name: str
    :param fmt: Message format to use
    :type fmt: str
    :return: Logger instance
    :rtype: Logger
    """
    log = realGetLogger(name)
    # Only enable colour if support was loaded properly
    handler = ColourStreamHandler()
    handler.setLevel(level)
    handler.setFormatter(Formatter(fmt))
    log.addHandler(handler)
    log.setLevel(level)
    log.propagate = 0  # Don't bubble up to the root logger
    return log

util_log = get_logger(__name__, level="DEBUG")

###############################################################################
# process simulated data
###############################################################################
def process_ts(variants, model_label, rep_label, mnm_frac, mnm_dist, mnm_num,
                method, out_dir):
    if method == "archie":
        eig_data = generateEigData(
            variants,
            model_label,
            rep_label=rep_label,
            mnm_frac=mnm_frac,
            mnm_dist=mnm_dist,
            mnm_num=mnm_num)

        util_log.debug("---Generating data without MNMs---")
        util_log.debug("Prefix: %s", eig_data.prefix)
        util_log.debug("Genotype matrix dimensions: %s, %s" % tuple(
            pd.DataFrame(eig_data.geno).shape))
        util_log.debug("SNP table dimensions: %s, %s" % tuple(
            pd.DataFrame(eig_data.snp).shape))
        writeEigData(eig_data, out_dir).dump()

        eig_data_mnms = eig_data.mnms()
        util_log.debug("---Generating data with MNMs---")
        util_log.debug("Prefix: %s", eig_data_mnms.prefix)
        util_log.debug("Genotype matrix dimensions: %s, %s" % tuple(
            eig_data_mnms.geno.shape))
        util_log.debug(
            "SNP table dimensions: %s, %s" % tuple(eig_data_mnms.snp.shape))
        writeEigData(eig_data_mnms, out_dir).dump()

    elif method == "sprime":
        vcf_data = generateVCFData(
            variants,
            model_label,
            rep_label=rep_label,
            mnm_frac=mnm_frac,
            mnm_dist=mnm_dist,
            mnm_num=mnm_num)

        util_log.debug("---Generating data without MNMs---")
        util_log.debug("Prefix: %s", vcf_data.prefix)
        util_log.debug(
            "VCF dimensions: %s, %s" % tuple(pd.DataFrame(vcf_data.vcf).shape))
        writeVCFData(vcf_data, out_dir).dump()

        vcf_data_mnms = vcf_data.mnms()
        util_log.debug("---Generating data with MNMs---")
        util_log.debug("Prefix: %s", vcf_data_mnms.prefix)
        util_log.debug("VCF dimensions: %s, %s" % tuple(
            pd.DataFrame(vcf_data_mnms.vcf).shape))
        writeVCFData(vcf_data_mnms, out_dir).dump()

###############################################################################
# classes for generating data in VCF format
###############################################################################
class mnmVCFData:
    """
    object defines .vcf matrices
    """

    def __init__(self, vcf, prefix, repeat_rows):
        self.vcf = vcf
        self.prefix = prefix
        self.repeat_rows = repeat_rows

class generateVCFData:
    """
    generate VCF files from tree sequence
    """

    def __init__(self,
                 variants,
                 model_label,
                 rep_label=0,
                 mnm_frac=0,
                 mnm_dist=0,
                 mnm_num=0):
        self.variants = variants
        self.model_label = model_label
        self.rep_label = rep_label
        if mnm_frac > 0:
            self.sim_mnms = True
        else:
            self.sim_mnms = False
        self.mnm_frac = mnm_frac
        self.mnm_dist = mnm_dist
        self.mnm_num = mnm_num
        self.vcf = self.vcf()
        self.prefix = self.prefix()

    def prefix(self):
        """
        get unique identifier for model based on inputs
        """

        prefix = self.model_label + "_rep" + str(self.rep_label)
        return prefix

    def vcf(self):
        """
        generate data frame of genotypes in VCF format
        """

        # for variant in self.ts.variants():
        #     pos = round(variant.site.position)
        # output current variant to VCF

        # text_file.write("\t".join(snv) + "\n")

        snvs = []
        # d = np.empty([59, 2], dtype=int)
        for variant in self.variants:
            pos = round(variant.site.position)
            snv = [
                "1",
                str(pos),
                "1_%s" % pos, "A", "G", "50", "PASS", "VT=SNP", "GT"
            ]
            snv.extend([("|").join(
                [str(g) for g in variant.genotypes[i:i + 2]])
                        for i in range(0, len(variant.genotypes), 2)])
            snvs.append(snv)

        # d = d.append(pd.DataFrame(snv, index=[0]), ignore_index=True)
        # snv_df = pd.DataFrame(snvs)
        # util_log.debug(tuple(pd.DataFrame(snvs).shape))

        return snvs

    def mnms(self):
        """
        update .vcf output to include simulated MNMs
        """

        # snv_df = pd.DataFrame()
        snv_df = []
        #         geno_mat = self.geno()
        repeat_rows = []
        for snv in self.vcf:
            snv_df.append(snv)

            random.seed(int(snv[1]))
            if random.random() < self.mnm_frac:
                if(self.mnm_num == 0):
                    num_clust = math.ceil(random.random() * 4) + 1
                else:
                    num_clust = self.mnm_num
                repeat_rows.append(num_clust)
                dist = random.randint(10, self.mnm_dist)

                for i in range(1, num_clust):

                    # make sure a mnm is at least 10bp from its counterpart.
                    # this is to avoid the internal filter of Sprime.
                    mnm_snv = snv
                    mnm_snv[2] = "1_%s" % snv[1]
                    mnm_cand = int(snv[1]) + dist * i
                    mnm_cand_r = str(round(mnm_cand))
                    mnm_snv[1] = str(mnm_cand_r)
                    snv_df.append(mnm_snv)
            else:
                repeat_rows.append(1)

        # util_log.debug("\t".join(snv_df[i]) for i in range(0, 4))
        mnm_prefix = self.prefix + "_mnm" + str(self.mnm_dist) + "-" + str(
            self.mnm_frac) + "-" + str(self.mnm_num)

        return mnmVCFData(
            vcf=snv_df, prefix=mnm_prefix, repeat_rows=repeat_rows)

class writeVCFData:
    """
    functions for writing .vcf files
    """

    def __init__(self, vcf_data, output_dir="./"):
        self.vcf_data = vcf_data
        self.prefix = vcf_data.prefix
        self.output_dir = output_dir
        self.out_file = self.output_dir + self.prefix + ".vcf"

    def dump(self):
        """
        run all 3 output functions: write_snp, write_geno, and write_ind
        """

        self.write_vcf()
        # self.write_geno()
        # self.write_ind()

    def write_vcf(self):
        """
        write .vcf files
        """

        header = [
            "#CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO",
            "FORMAT"
        ]

        l_IDs = []
        for popID in ["AFR", "EUR", "EA"]:
            #        dict_IDs[popID] = []
            for indID in range(50):
                #            dict_IDs[popID].append("%s_ind%s" % (popID, indID))
                l_IDs.append("%s_ind%s" % (popID, indID))

        header.extend(l_IDs)
        vcf_out = pd.DataFrame(self.vcf_data.vcf)
        colnames = dict(zip(range(0, len(header)), header))
        vcf_out.rename(columns=colnames, inplace=True)

        vcf_out_file = self.output_dir + self.prefix + ".vcf"
        util_log.debug("writing VCF to %s", vcf_out_file)
        vcf_out.to_csv(vcf_out_file, index=False, sep="\t")

###############################################################################
# classes for generating data in EIGENSTRAT format
###############################################################################

class mnmEigData:
    """
    object defines .geno and .snp matrices
    """

    def __init__(self, geno, snp, prefix, repeat_rows):
        self.geno = geno
        self.snp = snp
        self.prefix = prefix
        self.repeat_rows = repeat_rows

class generateEigData:
    """
    class describes the .snp, .geno, and .ind EIGENSTRAT formats
    """

    def __init__(self,
                 variants,
                 model_label,
                 rep_label=0,
                 mnm_frac=0,
                 mnm_dist=0,
                 mnm_num=0):
        # self.ts = ts
        self.variants = variants
        self.model_label = model_label
        self.rep_label = rep_label
        if mnm_frac > 0:
            self.sim_mnms = True
        else:
            self.sim_mnms = False
        self.mnm_frac = mnm_frac
        self.mnm_dist = mnm_dist
        self.mnm_num = mnm_num
        self.geno = self.geno()
        self.snp = self.snp()
        self.prefix = self.prefix()

    def prefix(self):
        """
        get unique identifier for model based on inputs
        """

        prefix = self.model_label + "_rep" + str(self.rep_label)
        #         if self.sim_mnms:
        #             prefix = prefix + "_mnm"+str(self.mnm_dist)+"-"+str(self.mnm_frac)

        return prefix

    def geno(self):
        """
        generate .geno file from tree sequence
        """

        # util_log.debug(len(self.variants[0].genotypes))
        geno = np.zeros((0, len(self.variants[0].genotypes)), dtype=np.int8)
        # geno = np.array([])
        # idx = 0
        for variant in self.variants:
            # util_log.debug(variant.genotypes)
            # util_log.debug(geno)
            # util_log.debug(geno[idx])
            geno = np.append(geno, np.asarray([variant.genotypes]), axis=0)
            # idx += 1

        # geno = np.delete(geno, (0), axis=0)  # remove dummy 1st row
        return geno

    def snp(self):
        """
        generate .snp file from tree sequence
        """

        d = pd.DataFrame(columns=['ID', 'CHR', 'POS1', 'POS', 'REF', 'ALT'])
        # for variant in list(self.ts.variants()):
        for variant in self.variants:
            d = d.append(
                pd.DataFrame({
                    'ID': "1:" + str(round(variant.site.position)),
                    'CHR': "1",
                    'POS1': str(variant.site.position / 10e6),
                    'POS': str(round(variant.site.position)),
                    'REF': "A",
                    'ALT': "G"
                },
                             index=[0]),
                ignore_index=True)

        snp_df = pd.DataFrame(d)
        return snp_df

    def mnms(self):
        """
        update .geno and .snp output to include simulated MNMs
        """

        snp_mnm = pd.DataFrame(
            columns=['ID', 'CHR', 'POS1', 'POS', 'REF', 'ALT'])
        #         geno_mat = self.geno()
        repeat_rows = []
        for index, snp in self.snp.iterrows():
            snp_mnm = snp_mnm.append(snp, ignore_index=True)

            random.seed(snp['POS'])
            if random.random() < self.mnm_frac:
                if(self.mnm_num == 0):
                    num_clust = math.ceil(random.random() * 4) + 1
                else:
                    num_clust = self.mnm_num
                repeat_rows.append(num_clust)

                dist = random.randint(1, self.mnm_dist)

                for i in range(1, num_clust):

                    mnm_snp = snp
                    mnm_snp['ID'] = "1:" + mnm_snp['POS']
                    mnm_snp['POS'] = str(round(int(snp['POS']) + dist * i))
                    mnm_snp['POS1'] = float(mnm_snp['POS']) / 10e6
                    snp_mnm = snp_mnm.append(mnm_snp, ignore_index=True)
            else:
                repeat_rows.append(1)

        geno_mat_mnm = np.repeat(self.geno, repeats=repeat_rows, axis=0)

        mnm_prefix = self.prefix + "_mnm" + str(self.mnm_dist) + "-" + str(
            self.mnm_frac)  + "-" + str(self.mnm_num)

        return mnmEigData(
            geno=geno_mat_mnm,
            snp=snp_mnm,
            prefix=mnm_prefix,
            repeat_rows=repeat_rows)

class writeEigData:
    """
    functions for writing .snp .geno and .ind files
    """

    def __init__(self, eig_data, output_dir="./"):
        self.eig_data = eig_data
        self.prefix = eig_data.prefix
        self.output_dir = output_dir
        self.out_file = self.output_dir + self.prefix + ".snp"

    def dump(self):
        """
        run all 3 output functions: write_snp, write_geno, and write_ind
        """

        self.write_snp()
        self.write_geno()
        self.write_ind()

    def write_snp(self):
        """
        write .snp files
        """

        snp_out_file = self.output_dir + self.prefix + ".snp"
        util_log.debug("writing .snp file to %s", snp_out_file)
        self.eig_data.snp.to_csv(snp_out_file, index=False, header=False, sep="\t")

    def write_geno(self):
        """
        write .geno files
        """

        geno_afr = self.eig_data.geno[:, :100]
        geno_eur = self.eig_data.geno[:, 100:200]

        geno_pop = [geno_afr, geno_eur]

        for i, pop in enumerate(["afr", "eur"]):
            geno_out_file = self.output_dir + self.prefix + "_" + pop + ".geno"
            util_log.debug("writing .geno file to %s", geno_out_file)
            np.savetxt(geno_out_file, geno_pop[i], delimiter="", fmt='%i')

    def write_ind(self):
        """
        write .ind files
        """

        for pop in ["afr", "eur"]:
            # write out separate .ind files per population.
            # columns indicate sample ID, sex (set as 'U'), and label (set as 'ADMIXED')
            ind_file = self.output_dir + self.prefix + "_" + pop + ".ind"
            util_log.debug("writing .ind file to %s", ind_file)
            with open(ind_file, "w") as id_file:
                for sample_id in range(0, 100):
                    sample_name = self.prefix + "_" + pop + \
                        "_sample_" + str(sample_id)
                    print("\t".join([sample_name, "U", pop]), file=id_file)


from collections import defaultdict

# Name: Longest Substring with At Most Two Distinct Characters
# Link: https://leetcode.com/problems/longest-substring-with-at-most-two-distinct-characters/
# Method: Sliding window with char counter
# Time: O(n)
# Space: O(1)
# Difficulty: Medium

class Solution:
    def lengthOfLongestSubstringTwoDistinct(self, s: str) -> int:
        if not s:
            return 0
        count = defaultdict(int, {s[0]: 1})
        start_idx = 0
        len_max = 1
        for i in range(1, len(s)):
            count[s[i]] += 1
            while len(count) >= 3 and start_idx <= i:
                count[s[start_idx]] -= 1
                if count[s[start_idx]] == 0:
                    del count[s[start_idx]]
                start_idx += 1
            len_max = max(i - start_idx + 1, len_max)
        return len_max


"""class Mayor():
    elif edad >= int(j["EdadInicio"]) and edad <= int(j["EdadFinal"]):
            fase = "2"
            print("fase 2")
            print("repit")
            print(j["EdadInicio"],j["EdadFinal"])
        elif edad >= int(j["EdadInicio"]) and edad <= int(j["EdadFinal"]):
            fase = "3"
            print("fase 3")
            print(j["EdadInicio"],j["EdadFinal"])
        elif edad >= int(j["EdadInicio"]) and edad <= int(j["EdadFinal"]):
            fase = "4"
            print("fase 4")
            print(j["EdadInicio"],j["EdadFinal"])
        elif edad >= int(j["EdadInicio"]) and edad <= int(j["EdadFinal"]):
            fase = "5"
            print("fase 5")
            print(j["EdadInicio"],j["EdadFinal"])
            with open("c:/Users/Lenovo5/Desktop/UNIVERSIDAD SAN PABLO DE GUATEMALA/PROGRAMACION/PROGRAMACIÓN II/GrupoEtario.json","r") as f:
    dato = json.load(f)
for i in dato["Grupos"]:
            print(i["EdadInicio"])

def AsignacionFase(edad):
    fase=""
    with open("c:/Users/Lenovo5/Desktop/UNIVERSIDAD SAN PABLO DE GUATEMALA/PROGRAMACION/PROGRAMACIÓN II/GrupoEtario.json","r") as f:
            dato = json.load(f)
    for j in range(0,5):
        #print("edad", self.edad)
        #print(j["EdadInicio"])
        if edad >= 60 and edad <= 150:
            fase = "1"
            print("fase 1")
            #print(j["EdadInicio"],j["EdadFinal"])
        elif edad >= 40 and edad <= 59:
            fase = "2"
            print("fase 2")
            print("repit")
            #print(j["EdadInicio"],j["EdadFinal"])
        elif edad >= 19 and edad <= 39:
            fase = "3"
            print("fase 3")
            #print(j["EdadInicio"],j["EdadFinal"])
        elif edad >= 12 and edad <= 18:
            fase = "4"
            print("fase 4")
            #print(j["EdadInicio"],j["EdadFinal"])
        elif edad >= 5 and edad <= 11:
            fase = "5"
            print("fase 5")
            #print(j["EdadInicio"],j["EdadFinal"])
    return fase

        self.nombres = ""
        self.apellidos=""
        self.fNacimiento = ""
        self.sexo = ""
        self.email = ""
        self.telefono =""
        self.eCongenita = ""
        self.eAutoinmune = ""
        self.edad = ""
        self.dosis =""
        self.fase = """
import json

def Archivo():
    data = {}
    data["Personas"] = []
    data ["Personas"].append({
            "nombres" :"nombres",
            "apellidos" :"apellidos",
            "edad" :"edad",
            "email" :"email",
            "telefono" :"telefono",
            "sexo" :"sexo",
            "eCongenita" :"eCongenita",
            "eAutoinmune" : "eAutoinmune",
            "fNacimiento" : "fNacimiento",
            "dosis" :"dosis",
            "Fase": "fase",
            "cita": "cita"})
    with open("RegistroPersonas.json", "w") as file:
        json.dump(data, file, indent=5, ensure_ascii=False)

Archivo()

def Archivo2():
    data = {}
    data["jugadores"] = []
    data ["jugadores"].append({
                "nombre" :"Prueba",
                "Contraseña": "123",
                "nacionalidad" :"Guatemalteca",
                "apodo" : "Prueba1",
                "edad" : "24",
                "estado" : "Inactiva",
                "Total_Puntos":"0"})
    with open("Prueba.json", "w") as file:
        json.dump(data, file, indent=5, ensure_ascii=False)

Archivo2()

"""FechaActual = datetime.datetime.now().strftime("%d/%m/%Y")
print(FechaActual)
with open("c:/Users/Lenovo5/Desktop/UNIVERSIDAD SAN PABLO DE GUATEMALA/PROGRAMACION/PROGRAMACIÓN II/GrupoEtario.json") as file:
    dato = json.load(file)
fechaC = fecha()
for j in dato["Grupos"]:
    FechaInicio = j["FechaInicio"]
    FechaInicio = datetime.datetime.strptime(FechaInicio, "%d/%m/%Y")
    FechaActual = datetime.now()
    if FechaInicio > FechaActual:
        print("Lo sentimos, la fecha no es aceptable")
#dia = fechaA.day
#mes = fechaA.month
#anio = fechaA.year
#fInicio = date(anio,mes,dia)
#fFinal = date(2022,11,30)"""

"""a = open("InfoCitas.txt","w")
a.write("0|0")
a.close()
print("Archivo creado")

lis =[]
def prueba(fInicio,fFinal):
    for i in range(int((fFinal - fInicio).days)):
        lis.append(fInicio + timedelta(i))
    print("La funcion de generacion de fechas esta lista")
    #print(fechas)
#print()
prueba(fInicio,fFinal)
fechas =[]
for dato in lis:
    fechas.append(dato)
    print(dato[3])
print(fechas)

def AbrirArchivotxt():
    a = open("InfoCitas.txt", "r")
    lineas= a.readlines()
    ListaDatos = []
    for linea in lineas:
        linea = linea.replace("\n", "").split("|")
        ListaDatos.append(linea)
    a.close()
    print("La funcion de Abrir Archivo esta lista")
    return ListaDatos

def GenerarCita(lista):
    lista = AbrirArchivotxt()
    NewCita = ""
    while True:
        for i in lista:
            NumCitas = int(i[0])
            PosicionF = int(i[1])
            if NumCitas <= 100:
                for j in lista:
                    NewCita = j[PosicionF]
                NumCitas =+ 1
                datos = str(NumCitas)+"|"+str(PosicionF)
                a = open("BaseDeDatos.txt","w")
                a.write(datos)
                a.close()
                break
            elif NumCitas > 100:
                PosicionF =+ 1
                datos = "0"+"|"+str(PosicionF)
                a = open("BaseDeDatos.txt","w")
                a.write(datos)
                a.close()
                break
        print("Su nueva cita es", NewCita)
        print("La Generacion de cita esta lista")
        return NewCita

#GenerarCita(fechas)  """

"""a = datetime.datetime.today()
numdays = 100
dateList = []
for x in range (0, numdays):
    dateList.append(fInicio - datetime.timedelta(days = x))
print(dateList)
for dato in dateList:
    print(dato.strftime("%Y-%m-%d"))

def generar(fInicio, fFinal):
    fechas = []
    dias = (fFinal-fInicio).days
    for i in range(dias):
        fechas.append(fInicio+timedelta(i))
        return fechas

resultado = generar(fInicio,fFinal)

for f in resultado:
    print(f)"""

"""def prueba(fInicio,fFinal):
    for i in range(int((fFinal - fInicio).days)):
        yield fInicio + timedelta(i)

for dato in prueba(fInicio,fFinal):
    print(dato.strftime("%Y-%m-%d"))

def daterange(start_date, end_date):
    for n in range(int ((end_date - start_date).days)):
        yield start_date + timedelta(n)

start_date = date(2013, 1, 1)
end_date = date(2015, 6, 2)
for single_date in daterange(start_date, end_date):
    print(single_date.strftime("%Y-%m-%d"))

dia = int(input("Día: "))
mes = int(input("Mes: "))
anio = int(input("Año: "))

fechaNacimiento = date(anio,mes,dia)

calculo = fechaA.year - fechaNacimiento.year
calculo -= ((fechaA.month, fechaA.day) < (fechaNacimiento.month, fechaNacimiento.day))

print("Tu edad es: ", calculo)"""

"""class Prueba():
    def __init__(self):
        self.__variable= 2

    def operacion(self):
        #print("adentro")
        while self.__variable > 1:
            print("Logrado")

objeto = Prueba()
objeto.operacion()
import json
#import datetime
from datetime import date
from pruebas0 import fecha, ValidacionFecha, GrabarDatos, AsignacionFase
#from datetime import date
#from random import randint

class Persona():
    def __init__(self):
        #fechaA = datetime.datetime.now()
        #dia = fechaA.day
        #mes = fechaA.month
        #anio = fechaA.year
        self.fechaAct= date.today()
        self.nombres = ""
        self.apellidos=""
        self.fNacimiento = ""
        self.sexo = ""
        self.email = ""
        self.telefono =""
        self.eCongenita = ""
        self.eAutoinmune = ""
        self.edad = 0
        self.fase = ""

    def registro(self):
        self.nombres = input("Nombres: ")
        self.apellidos = input("Apellidos: ")
        print("Fecha de Nacimiento: (en números)")
        dia = int(input("Día: "))
        mes = int(input("Mes: "))
        anio = int(input("Año: "))
        self.fNacimiento = date(anio,mes,dia)
        self.sexo= input("Sexo: ")
        self.email = input("Correo Electrónico: ")
        self.telefono = input("No. Telefóno/Celular: ")
        val = input("¿Padeces de alguna enfermedad Congénita?: ").upper()
        if val == "SI" or val =="SÍ":
            self.eCongenita = True
        elif val == "NO":
            self.eCongenita = False
        else:
            self.eCongenita = "No especificado"
        val2 = input("¿Padeces de alguna enfermedad autoinmune?: ").upper()
        if val2 == "SI" or val =="SÍ":
            self.eAutoinmune = True
        elif val2 == "NO":
            self.eAutoinmune = False
        else:
            self.eAutoinmune = "No especificado"
        self.dosis = input("Dosis: ")
        calculo = self.fechaAct.year - self.fNacimiento.year
        calculo -= ((self.fechaAct.month, self.fechaAct.day) < (self.fNacimiento.month, self.fNacimiento.day))
        self.edad = calculo
        print("calculo", calculo)
        with open("c:/Users/Lenovo5/Desktop/UNIVERSIDAD SAN PABLO DE GUATEMALA/PROGRAMACION/PROGRAMACIÓN II/GrupoEtario.json","r") as f:
            dato = json.load(f)
        print(dato)

class Fase1(Persona):
    def __init__(self):
        Persona().__init__()
        self.fase = AsignacionFase(int(self.edad))
    def GenerarCita(self):
        acceso = ValidacionFecha(self.fase)
        if acceso == True: #>= self.edad and j["EdadFinal"]  <= self.edad:
            self.cita = fecha()
            GrabarDatos(self.nombres,self.apellidos,str(self.edad),self.email,self.telefono,self.sexo,self.eCongenita,self.eAutoinmune,self.fNacimiento,self.dosis,self.fase,self.cita)
            print("Su cita es la fecha ", self.cita)
            print("Por favor llevar su documento de identificación")
        elif acceso == False:
            print("su fase no comienza")

class Fase2(Persona):
    pass
    #def Cita(self):

class Fase3(Persona):
    pass
    #def Cita(self):

class Fase4(Persona):
    pass
    #def Cita(self):

class Fase5(Persona):
    pass
    #def Cita(self):

#Nombres,teléfono, padece enfermedad congénita(S,N), padece enfermedad autoinmune(S,N), Dosis(1,2)
objeto = Persona()
objeto.registro()
objeto2 = Fase1()
objeto2.GenerarCita()"""


# ----------------------------------------------------------------------
# |
# |  Binary_BuildVerificationTest.py
# |
# |  David Brownell <db@DavidBrownell.com>
# |      2022-11-22 10:42:13
# |
# ----------------------------------------------------------------------
# |
# |  Copyright David Brownell 2022-23
# |  Distributed under the Boost Software License, Version 1.0. See
# |  accompanying file LICENSE_1_0.txt or copy at
# |  http://www.boost.org/LICENSE_1_0.txt.
# |
# ----------------------------------------------------------------------
"""Build verification test for binaries"""

# Note that this file will be invoked outside of an activated environment and cannot take a dependency
# on anything in this repository or Common_Foundation.

import os
import subprocess
import stat
import sys
import textwrap

from pathlib import Path
from typing import List, Optional, Set

# ----------------------------------------------------------------------
def EntryPoint(
    args: List[str],
) -> int:
    if len(args) != 2:
        sys.stdout.write(
            textwrap.dedent(
                """\
                ERROR: Usage:

                    {} <temp_directory>

                """,
            ).format(
                args[0],
            ),
        )

        return -1

    temp_directory = Path(args[1])
    assert temp_directory.is_dir(), temp_directory

    build_output_dir = Path(temp_directory)

    # Get the Backup binary
    for potential_dir in [
        "artifacts",
        "Scripts",
        "Backup",
        "Build",
    ]:
        potential_build_output_dir = build_output_dir / potential_dir
        if not potential_build_output_dir.is_dir():
            break

        build_output_dir = potential_build_output_dir

    backup_filename = build_output_dir / "Backup"

    if not backup_filename.is_file():
        potential_backup_filename = backup_filename.with_suffix(".exe")

        if potential_backup_filename.is_file():
            backup_filename = potential_backup_filename

    if not backup_filename.is_file():
        raise Exception("The filename '{}' does not exist.\n".format(backup_filename))

    # https://github.com/actions/upload-artifact/issues/38
    # Permissions are not currently being saved when uploading artifacts, so they must be set here.
    # This will eventually be fixed, which is why I am placing the work around here rather than in
    # the artifact upload- or download-code.

    backup_filename.chmod(stat.S_IXUSR | stat.S_IWUSR | stat.S_IRUSR)

    # Execute Tests
    result = _ValidateMirror(backup_filename, temp_directory)
    if result != 0:
        return result

    return 0

# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
def _ValidateMirror(
    backup_filename: Path,
    temp_directory: Path,
) -> int:
    source_dir = Path(__file__).parent.parent.parent.parent
    destination = source_dir.parent / "destination"

    command_line = '"{}" mirror execute "{}" "{}"'.format(
        backup_filename,
        destination,
        source_dir,
    )

    sys.stdout.write("Command Line: {}\n\n".format(command_line))

    result = subprocess.run(
        command_line,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )

    content = result.stdout.decode("utf-8")

    sys.stdout.write(content)

    if result.returncode != 0:
        return result.returncode

    # Compare the source and destination files

    # ----------------------------------------------------------------------
    def DecorateFilename(
        filename: Path,
        root: Path,
        prefix_to_strip: Optional[Path]=None,
    ) -> Path:
        filename = Path(*filename.parts[len(root.parts):])

        if prefix_to_strip:
            len_prefix_to_strip_parts = len(prefix_to_strip.parts)

            assert filename.parts[:len_prefix_to_strip_parts] == prefix_to_strip.parts, (filename.parts, prefix_to_strip.parts)
            filename = Path(*filename.parts[len_prefix_to_strip_parts:])

        return filename

    # ----------------------------------------------------------------------
    def GetFiles(
        root: Path,
        prefix_to_strip: Optional[Path]=None,
    ) -> Set[Path]:
        if prefix_to_strip:
            if prefix_to_strip.parts[0].endswith(":") or prefix_to_strip.parts[0].endswith(":\\"):
                prefix_to_strip = Path(prefix_to_strip.parts[0].replace(":", "_").rstrip("\\")) / Path(*prefix_to_strip.parts[1:])
            else:
                assert prefix_to_strip.parts[0] == "/", prefix_to_strip.parts
                prefix_to_strip = Path(*prefix_to_strip.parts[1:])

        results: Set[Path] = set()

        for this_root, directories, filenames in os.walk(root):
            this_root = Path(this_root)

            if not directories and not filenames:
                results.add(DecorateFilename(this_root, root, prefix_to_strip))

            for filename in filenames:
                results.add(DecorateFilename(this_root / filename, root, prefix_to_strip))

        return results

    # ----------------------------------------------------------------------

    source_files = GetFiles(source_dir)
    destination_files = GetFiles(destination / "Content", source_dir)

    if source_files != destination_files:
        sys.stdout.write("Source Files:\n{}\n".format("".join("  - {}) {}\n".format(index, source_file) for index, source_file in enumerate(source_files))))
        sys.stdout.write("Destination Files:\n{}\n".format("".join("  - {}) {}\n".format(index, destination_file) for index, destination_file in enumerate(destination_files))))

        return -1

    return 0

# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
if __name__ == "__main__":
    sys.exit(EntryPoint(sys.argv))


from datetime import datetime
import os

def logging(arg):
    path = arg

    def outer(func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            if not any(args):
                arguments = kwargs
            elif not any(kwargs):
                arguments = args
            else:
                arguments = args, kwargs
            with open(f'{path}logger.txt', 'a+', encoding='utf-8') as f:
                start = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                writer = f.write(f'Name: {func.__name__}, arguments: {arguments},'
                                 f' call_time: {start}, result: {result}\n')
                return result
        return wrapper
    return outer

@logging('C:/Users/Ярослав/PycharmProjects/pythonProject/decorators/')
def some_func(*args, **kwargs):
    print('Выполнено суммирование аргументов')
    return sum(args)

@logging('C:/Users/Ярослав/PycharmProjects/pythonProject/decorators/')
def name_printing(*args, **kwargs):
    return args, kwargs

if __name__ == '__main__':
    some_func(1, 1, 1)
    some_func(1, 2, 3, 4, 5)
    name_printing(1, first_name='Max', lastname='Hodokov')


#!/usr/bin/env python

from setuptools import setup, find_packages
import sys, os

version = '0.02'

setup(name='Bert',
      version=version,
      description="A game editor",
      long_description="""A game editor""",
      author='Oliver Dashiell Bunyan',
      author_email='dashb@me.co.uk',
      url='http://www.blueshed.co.uk/bert',
      packages=find_packages('src',exclude=['*tests*']),
      package_dir = {'':'src'},
      include_package_data = True,
      exclude_package_data = { '': ['tests/*'] },
      install_requires = [
        'setuptools',
        'tornado>=2.4',
        'sqlalchemy'
      ],
      entry_points = {
      'console_scripts' : [
                           'bert = bert.web:main'
                           ]
      })


from __future__ import annotations
import typing
from typing import Any, Callable, Dict, List, Set, Tuple
if typing.TYPE_CHECKING:
    # Avoid circular referencing
    from server.client_manager import ClientManager
    from server.party_manager import PartyManager
    from server.tsuserver import TsuserverDR
    from server.zone_manager import ZoneManager

import asyncio
import time

from server import logger
from server.constants import Constants
from server.evidence import EvidenceList
from server.exceptions import AreaError, ServerError
from server.subscriber import Publisher

from server.validate.areas import ValidateAreas

class AreaManager:
    """
    Create a new manager for the areas in a server.
    Contains the Area object definition, as well as the server's area list.
    """

    class Area:
        """
        Create a new area for the server.
        """

        def __init__(self, area_id: int, server: TsuserverDR, parameters: Dict[str, Any]):
            """
            Parameters
            ----------
            area_id: int
                The area ID.
            server: server.TsuserverDR
                The server this area belongs to.
            parameters: dict
                Area parameters as specified in the loaded area list.
            """

            self._clients = set()
            self.id = area_id
            self.server = server
            self.publisher = Publisher(self)

            self.invite_list = {}
            self.music_looper = None
            self.next_message_time = 0
            self.hp_def = 10
            self.hp_pro = 10
            self.doc = 'No document.'
            self.status = 'IDLE'
            self.judgelog = []
            self.shoutlog = []
            self.current_music = ''
            self.current_music_player = ''
            self.current_music_source = ''
            self.evi_list = EvidenceList()
            self.is_recording = False
            self.recorded_messages = []
            self.owned = False
            self.ic_lock = False
            self.is_locked = False
            self.is_gmlocked = False
            self.is_modlocked = False
            self.bleeds_to = set()
            self.blood_smeared = False
            self.lights = True
            self.last_ic_messages = list()
            self.parties = set()
            self.dicelog = list()
            self.lurk_length = 0
            self._in_zone = None
            self.noteworthy = False

            self.name = parameters['area']
            self.background = parameters['background']
            self.background_tod = parameters['background_tod']
            self.bg_lock = parameters['bglock']
            self.evidence_mod = parameters['evidence_mod']
            self.locking_allowed = parameters['locking_allowed']
            self.iniswap_allowed = parameters['iniswap_allowed']
            self.rp_getarea_allowed = parameters['rp_getarea_allowed']
            self.rp_getareas_allowed = parameters['rp_getareas_allowed']
            self.rollp_allowed = parameters['rollp_allowed']
            self.reachable_areas = parameters['reachable_areas']
            self.change_reachability_allowed = parameters['change_reachability_allowed']
            self.default_change_reachability_allowed = parameters['change_reachability_allowed']
            self.gm_iclock_allowed = parameters['gm_iclock_allowed']
            self.afk_delay = parameters['afk_delay']
            self.afk_sendto = parameters['afk_sendto']
            self.global_allowed = parameters['global_allowed']
            self.lobby_area = parameters['lobby_area']
            self.private_area = parameters['private_area']
            self.scream_range = parameters['scream_range']
            self.restricted_chars = parameters['restricted_chars']
            self.default_description = parameters['default_description']
            self.has_lights = parameters['has_lights']
            self.cbg_allowed = parameters['cbg_allowed']
            self.song_switch_allowed = parameters['song_switch_allowed']
            self.bullet = parameters['bullet']

            # Store the current description separately from the default description
            self.description = self.default_description
            # Have a background backup in order to restore temporary background changes
            self.background_backup = self.background

            self.default_reachable_areas = self.reachable_areas.copy()
            self.visible_reachable_areas = self.reachable_areas.copy()

            self.reachable_areas.add(self.name) # Area can always reach itself

        @property
        def clients(self) -> Set[ClientManager.Client]:
            """
            Declarator for a public clients attribute.
            """

            return self._clients

        @clients.setter
        def clients(self, new_clients) -> Set[ClientManager.Client]:
            """
            Set the clients parameter to the given one.

            Parameters
            ----------
            new_clients: Set[ClientManager.Client]
                New set of clients.
            """

            self._clients = new_clients

        def new_client(self, client: ClientManager.Client):
            """
            Add a client to the client list of the current area.

            Parameters
            ----------
            client: ClientManager.Client
                Client to add.
            """

            self.clients.add(client)

        def remove_client(self, client: ClientManager.Client):
            """
            Remove a client of the client list of the current area.

            Parameters
            ----------
            client: server.ClientManager.Client
                Client to remove.

            Raises
            ------
            KeyError
                If the client is not in the area list.
            """

            try:
                self.clients.remove(client)
            except KeyError:
                if not client.id == -1: # Ignore pre-clients (before getting playercount)
                    info = 'Area {} does not contain client {}'.format(self, client)
                    raise KeyError(info)

            if not self.clients:
                self.unlock()

        def send_command(self, cmd: str, *args: List):
            """
            Send a network packet to all clients in the area.

            Parameters
            ----------
            cmd: str
                ID of the packet.
            *args
                Packet arguments.
            """

            for c in self.clients:
                c.send_command(cmd, *args)

        def send_command_dict(self, cmd: str, dargs: Dict[str, Any]):
            """
            Send a network packet to all clients in the area.

            Parameters
            ----------
            cmd: str
                ID of the packet.
            dargs : dict of str to Any
                Packet argument as a map of argument name to argument value.
            """

            for client in self.clients:
                client.send_command_dict(cmd, dargs)

        def broadcast_ooc(self, msg: str):
            """
            Send an OOC server message to the clients in the area.

            Parameters
            ----------
            msg: str
                Message to be sent.
            """

            for client in self.clients:
                client.send_ooc(msg)

        def broadcast_ic_attention(self, cond: Callable[[ClientManager.Client], bool] = None):
            """
            Send an IC message with a ding to everyone in the area indicating something catches
            their attention, *except* if the player is blind or deaf, or if the area is a lobby
            area.

            Parameters
            ----------
            cond : types.LambdaType: ClientManager.Client -> bool, optional
                Custom condition each player in the area must also satisfy to receive the
                attention message.

            Returns
            -------
            None.

            """

            if self.lobby_area:
                return

            if cond is None:
                cond = lambda client: True
            for player in self.clients:
                if player.is_deaf and player.is_blind:
                    continue

                if cond(player):
                    player.send_ic_attention()

        def get_background_tod(self) -> Dict[str, str]:
            if not self.lights:
                return dict()

            return self.background_tod.copy()

        def change_background(self, bg: str, validate: bool = True, override_blind: bool = False):
            """
            Change the background of the current area.

            Parameters
            ----------
            bg: str
                New background name.
            validate: bool, optional
                Whether to first determine if background name is listed as a server background
                before changing. Defaults to True.
            override_blind: bool, optional
                Whether to send the intended background to blind people as opposed to the server
                blackout one. Defaults to False (send blackout).

            Raises
            ------
            AreaError
                If the server attempted to validate the background name and failed.
            """

            if validate and bg.lower() not in [name.lower() for name in self.server.backgrounds]:
                raise AreaError('Invalid background name.')

            if self.lights:
                self.background = bg
            else:
                self.background = self.server.config['blackout_background']
                self.background_backup = bg
            for c in self.clients:
                if c.is_blind and not override_blind:
                    c.send_background(name=self.server.config['blackout_background'])
                else:
                    c.send_background(name=self.background,
                                      tod_backgrounds=self.get_background_tod())

        def get_chars_unusable(self, allow_restricted: bool = False,
                               more_unavail_chars: Set[int] = None) -> Set[int]:
            """
            Obtain all characters that a player in the current area may NOT change to.

            Parameters
            ----------
            allow_restricted: bool, optional
                Whether to include characters whose usage has been manually restricted in the area.
                Defaults to False.
            more_unavail_chars: set of int, optional
                Additional characters to mark as taken (and thus unusuable) in the area. Defaults
                to None.

            Returns
            -------
            unavailable: set of int
                Character IDs of all unavailable characters in the area.
            """

            if more_unavail_chars is None:
                more_unavail_chars = set()

            unavailable = {x.char_id for x in self.clients if x.char_id is not None
                           and x.char_id >= 0}
            unavailable |= more_unavail_chars
            restricted = {self.server.char_list.index(name) for name in self.restricted_chars}

            if not allow_restricted:
                unavailable |= restricted

            return unavailable

        def get_rand_avail_char_id(self, allow_restricted: bool = False,
                                   more_unavail_chars: Set[int] = None) -> int:
            """
            Obtain a random available character in the area.

            Parameters
            ----------
            allow_restricted: bool, optional
                Whether to include characters whose usage has been manually restricted in the area.
                Defaults to false.
            more_unavail_chars: set of int, optional
                Additional characters to mark as taken (and thus unsuable) in the area. Defaults to
                None.

            Returns
            -------
            int
                ID of randomly chosen available character in the area.

            Raises
            -------
            AreaError
                If there are no available characters in the area.
            """

            unusable = self.get_chars_unusable(allow_restricted=allow_restricted,
                                               more_unavail_chars=more_unavail_chars)
            available = {i for i in range(len(self.server.char_list)) if i not in unusable}

            if not available:
                raise AreaError('No available characters.')

            return self.server.random.choice(tuple(available))

        def is_char_available(self, char_id: int, allow_restricted: bool = False,
                              more_unavail_chars: Set[int] = None) -> bool:
            """
            Decide whether a character can be selected in the current area.

            Parameters
            ----------
            char_id: int
                ID of the character to test.
            allow_restricted: bool, optional
                Whether to include characters whose usage has been manually restricted in the area.
                Defaults to False.
            more_unavail_chars: set, optional
                Additional characters to mark as taken in the area. Defaults to None.

            Returns
            -------
            bool
                True if tested character ID is the spectator ID (which is always available), or
                is not found to be among the area's unusable characters.
            """

            unused = char_id in self.get_chars_unusable(allow_restricted=allow_restricted,
                                                        more_unavail_chars=more_unavail_chars)
            return char_id == -1 or not unused

        def add_to_dicelog(self, client: ClientManager.Client, msg: str):
            """
            Add a dice roll to the dice log of the area.

            Parameters
            ----------
            client: server.ClientManager.Client
                Client to record.
            msg: str
                Dice log to record.
            """

            if len(self.dicelog) >= 20:
                self.dicelog = self.dicelog[1:]

            info = '{} | [{}] {} ({}) {}'.format(Constants.get_time(), client.id,
                                                 client.displayname, client.get_ip(), msg)
            self.dicelog.append(info)

        def get_dicelog(self) -> str:
            """
            Return the dice log of the area.
            """

            info = '== Dice log of area {} ({}) =='.format(self.name, self.id)

            if not self.dicelog:
                info += '\r\nNo dice have been rolled since the area was loaded.'
            else:
                for log in self.dicelog:
                    info += '\r\n*{}'.format(log)
            return info

        def change_doc(self, doc: str = 'No document.'):
            """
            Changes the casing document of the area, usually a URL.

            Parameters
            ----------
            doc: str, optional
                New casing document of the area. Defaults to 'No document.'
            """
            self.doc = doc

        def get_evidence_list(self, client: ClientManager.Client):
            """
            Obtain the evidence list for a client.

            Parameters
            ----------
            client: server.ClientManager.Client
                Client to target.
            """

            client.evi_list, evi_list = self.evi_list.create_evi_list(client)
            return evi_list

        def broadcast_evidence_list(self):
            """
            Resend all clients in the area their evidence list.

            Packet format: LE#<name>&<desc>&<img>#<name>
            """

            for client in self.clients:
                client.send_evidence_list()

        def change_hp(self, side: int, health: int):
            """
            Change a penalty healthbar.

            Parameters
            ----------
            side: int
                Penalty bar to change (1 for def, 2 for pro).
            health: int
                New health value of the penalty bar.

            Raises
            ------
            AreaError
                If an invalid penalty bar or health value was given.

            """

            if not 1 <= side <= 2:
                raise AreaError('Invalid penalty side.')
            if not 0 <= health <= 10:
                raise AreaError('Invalid penalty value.')

            if side == 1:
                self.hp_def = health
            elif side == 2:
                self.hp_pro = health

            for client in self.clients:
                client.send_health(side=side, health=health)

        def is_iniswap(self, client: ClientManager.Client, anim1: str, anim2: str,
                       char: str) -> bool:
            """
            Decide if a client is iniswapping or using files outside their claimed character folder.

            Assumes that server permitted iniswaps do not count as iniswaps.

            Parameters
            ----------
            client: ClientManager.Client
                Client to test.
            anim1: str
                Location of the preanimation the client used.
            anim2: str
                Location of the main animation the client used.
            char: str
                Name of the folder the client claims their files are.

            Returns
            -------
            bool
                True if either anim1 or anim2 point to an external location through '../../' or
                their claimed character folder does not match the expected server name and the
                performed iniswap is not in the list of allowed iniswaps by the server.
            """

            if char == client.get_char_name():
                return False

            if '..' in anim1 or '..' in anim2:
                return True
            for char_link in self.server.allowed_iniswaps:
                if client.get_char_name() in char_link and char in char_link:
                    return False
            return True

        def add_to_judgelog(self, client: ClientManager.Client, msg: str):
            """
            Add a judge action to the judge log of the area.

            Parameters
            ----------
            client: ClientManager.Client
                Client to record.
            msg: str
                Judge action to record.
            """

            if len(self.judgelog) >= 20:
                self.judgelog = self.judgelog[1:]

            info = '{} | [{}] {} ({}) {}'.format(Constants.get_time(), client.id,
                                                 client.displayname, client.get_ip(), msg)
            self.judgelog.append(info)

        def get_judgelog(self) -> str:
            """
            Return the judge log of the area.
            """

            info = '== Judge log of {} ({}) =='.format(self.name, self.id)

            if not self.judgelog:
                info += '\r\nNo judge actions have been performed since the area was loaded.'
            else:
                for log in self.judgelog:
                    info += '\r\n*{}'.format(log)
            return info

        def change_lights(self, new_lights: bool, initiator: ClientManager.Client = None,
                          area: AreaManager.Area = None):
            """
            Change the light status of the area and send related announcements.

            This also updates the light status for parties.

            Parameters
            ----------
            new_lights: bool
                New light status
            initiator: server.ClientManager.Client, optional
                Client who triggered the light status change.
            area: server.AreaManager.Area, optional
                Broadcasts light change messages to chosen area. Used if
                the initiator is elsewhere, such as in /zone_lights.
                If not None, the initiator will receive no notifications of
                light status changes.

            Raises
            ------
            AreaError
                If the new light status matches the current one.
            """

            status = {True: 'on', False: 'off'}
            if self.lights == new_lights:
                raise AreaError('The lights are already turned {}.'.format(status[new_lights]))

            # Change background to match new status
            if new_lights:
                if self.background == self.server.config['blackout_background']:
                    intended_background = self.background_backup
                else:
                    intended_background = self.background
            else:
                if self.background != self.server.config['blackout_background']:
                    self.background_backup = self.background
                intended_background = self.background

            self.lights = new_lights
            self.change_background(intended_background, validate=False) # Allow restoring custom bg.

            # Announce light status change
            if initiator: # If a player initiated the change light sequence, send targeted messages
                if area is None:
                    if not initiator.is_blind:
                        initiator.send_ooc('You turned the lights {}.'.format(status[new_lights]))
                    elif not initiator.is_deaf:
                        initiator.send_ooc('You hear a flicker.')
                    else:
                        initiator.send_ooc('You feel a light switch was flipped.')

                initiator.send_ooc_others('The lights were turned {}.'.format(status[new_lights]),
                                          is_zstaff_flex=False, in_area=area if area else True, to_blind=False)
                initiator.send_ooc_others('You hear a flicker.', is_zstaff_flex=False, in_area=area if area else True,
                                          to_blind=True, to_deaf=False)
                initiator.send_ooc_others('(X) {} [{}] turned the lights {}.'
                                          .format(initiator.displayname, initiator.id,
                                                  status[new_lights]),
                                          is_zstaff_flex=True, in_area=area if area else True)
            else: # Otherwise, send generic message
                self.broadcast_ooc('The lights were turned {}.'.format(status[new_lights]))

            # Notify the parties in the area that the lights have changed
            for party in self.parties:
                party.check_lights()

            for c in self.clients:
                found_something = c.area_changer.notify_me_rp(self, changed_visibility=True,
                                                              changed_hearing=False)
                if found_something and new_lights:
                    c.send_ic_attention()

        def set_next_msg_delay(self, msg_length: int):
            """
            Set a message delay for the next IC message in the area based on the length of the
            current message, so new messages sent before this delay expires are discarded.

            Parameters
            ----------
            msg_length: int
                Length of the current message.
            """

            delay = min(3000, 100 + 60 * msg_length)
            self.next_message_time = round(time.time() * 1000.0 + delay)

        def can_send_message(self) -> bool:
            """
            Decide if an incoming IC message does not violate the area's established delay for
            the previously received IC message.

            Returns
            -------
            bool
                True if the message was sent after the delay was over.
            """

            return (time.time() * 1000.0 - self.next_message_time) > 0

        def play_track(self, name: str, client: ClientManager.Client,
                       raise_if_not_found: bool = False, reveal_sneaked: bool = False,
                       pargs: Dict[str, Any] = None):
            """
            Wrapper function to play a music track in an area.

            Parameters
            ----------
            name : str
                Name of the track to play
            client : ClientManager.Client
                Client who initiated the track change request.
            effect : int, optional
                Accompanying effect to the track (only used by AO 2.8.4+). Defaults to 0.
            raise_if_not_found : bool, optional
                If True, it will raise ServerError if the track name is not in the server's music
                list nor the client's music list. If False, it will not care about it. Defaults to
                False.
            reveal_sneaked : bool, optional
                If True, it will change the visibility status of the sender client to True (reveal
                them). If False, it will keep their visibility as it was. Defaults to False.
            pargs : dict of str to Any
                If given, they are arguments to an MC packet that was given when the track was
                requested, and will override any other arguments given. If not, this is ignored.
                Defaults to None (and converted to an empty dictionary).

            Raises
            ------
            ServerError.FileInvalidNameError:
                If `name` references parent or current directories (e.g. "../hi.mp3")
            ServerError.MusicNotFoundError:
                If `name` is not a music track in the server or client's music list and
                `raise_if_not_found` is True.
            ServerError (with code 'FileInvalidName')
                If `name` references parent or current directories (e.g. "../hi.mp3")
            """

            if not pargs:
                pargs = dict()
            if Constants.includes_relative_directories(name):
                info = f'Music names may not reference parent or current directories: {name}'
                raise ServerError.FileInvalidNameError(info)

            try:
                name, length, source = self.server.get_song_data(name, c=client)
            except ServerError.MusicNotFoundError:
                if raise_if_not_found:
                    raise
                name, length, source = name, -1, ''

            if 'name' not in pargs:
                pargs['name'] = name
            if 'char_id' not in pargs:
                pargs['char_id'] = client.char_id
            pargs['showname'] = client.showname  # Ignore AO shownames
            if 'loop' not in pargs:
                pargs['loop'] = -1
            if 'channel' not in pargs:
                pargs['channel'] = 0
            if 'effects' not in pargs:
                pargs['effects'] = 0

            def loop(char_id):
                for client in self.clients:
                    loop_pargs = pargs.copy()
                    # Overwrite in case char_id changed (e.g., server looping)
                    loop_pargs['char_id'] = char_id
                    client.send_music(**loop_pargs)

                if self.music_looper:
                    self.music_looper.cancel()
                if length > 0:
                    f = lambda: loop(-1) # Server should loop now
                    self.music_looper = asyncio.get_event_loop().call_later(length, f)
            loop(pargs['char_id'])

            # Record the character name and the track they played.
            self.current_music_player = client.displayname
            self.current_music = name
            self.current_music_source = source

            logger.log_server('[{}][{}]Changed music to {}.'
                              .format(self.id, client.get_char_name(), name), client)

            # Changing music reveals sneaked players, so do that if requested
            if not client.is_staff() and not client.is_visible and reveal_sneaked:
                client.change_visibility(True)
                client.send_ooc_others('(X) {} [{}] revealed themselves by playing music ({}).'
                                       .format(client.displayname, client.id, client.area.id),
                                       is_zstaff=True)

        def play_music(self, name: str, char_id: int, length: int = -1, showname: str = ''):
            """
            Start playing a music track in an area.

            Parameters
            ----------
            name: str
                Name of the track to play.
            char_id: int
                Character ID of the player who played the track, or -1 if the server initiated it.
            length: int, optional
                Length of the track in seconds to allow for seamless server-managed looping.
                Defaults to -1 (no looping).
            showname: str, optional
                Showname to include with the notification of changed music. Defaults to '' (use
                default showname of character).
            """

            for client in self.clients:
                client.send_music(name=name, char_id=char_id, showname=showname)

            if self.music_looper:
                self.music_looper.cancel()
            if length > 0:
                f = lambda: self.play_music(name, -1, length)
                self.music_looper = asyncio.get_event_loop().call_later(length, f)

        def add_to_shoutlog(self, client: ClientManager.Client, msg: str):
            """
            Add a shout message to the shout log of the area.

            Parameters
            ----------
            client: ClientManager.Client
                Client to record.
            msg: str
                Shout message to record.
            """

            if len(self.shoutlog) >= 20:
                self.shoutlog = self.shoutlog[1:]

            info = '{} | [{}] {} ({}) {}'.format(Constants.get_time(), client.id,
                                                 client.displayname, client.get_ip(), msg)
            self.shoutlog.append(info)

        def add_party(self, party: PartyManager.Party):
            """
            Adds a party to the area's party list.

            Parameters
            ----------
            party: PartyManager.Party
                Party to record.

            Raises
            ------
            AreaError:
                If the party is already a part of the party list.
            """

            if party in self.parties:
                raise AreaError('Party {} is already part of the party list of this area.'
                                .format(party.get_id()))
            self.parties.add(party)

        def remove_party(self, party: PartyManager.Party):
            """
            Removes a party from the area's party list.

            Parameters
            ----------
            party: server.PartyManager.Party
                Party to record.

            Raises
            ------
            AreaError:
                If the party is not part of the party list.
            """

            if party not in self.parties:
                raise AreaError('Party {} is not part of the party list of this area.'
                                .format(party.get_id()))
            self.parties.remove(party)

        def get_shoutlog(self) -> str:
            """
            Get the shout log of the area.
            """
            info = '== Shout log of {} ({}) =='.format(self.name, self.id)

            if not self.shoutlog:
                info += '\r\nNo shouts have been performed since the area was loaded.'
            else:
                for log in self.shoutlog:
                    info += '\r\n*{}'.format(log)
            return info

        def change_status(self, value: str):
            """
            Change the casing status of the area to one of predetermined values.

            Parameters
            ----------
            value: str
                New casing status of the area.

            Raises
            ------
            AreaError
                If the new casing status is not among the allowed values.
            """

            allowed_values = ['idle', 'building-open', 'building-full', 'casing-open',
                              'casing-full', 'recess']
            if value.lower() not in allowed_values:
                raise AreaError('Invalid status. Possible values: {}'
                                .format(', '.join(allowed_values)))
            self.status = value.upper()

        def get_clock_creator(self) -> ClientManager.Client:
            """
            Return a client that has an active day cycle involving the current area.
            If multiple clients satisfy this condition, it returns the client with the smallest ID.
            If no clients satisfy the condition, it raises AreaError.ClientNotFound.

            Returns
            -------
            ClientManager.Client
                Client that has an active day cycle involving the current area.

            Raises
            ------
            AreaError.ClientNotFound
                If no client has an active day cycle involving the current area.
            """

            for client in self.server.get_clients():
                try:
                    args = self.server.tasker.get_task_args(client, ['as_day_cycle'])
                except KeyError:
                    pass
                else:
                    area_1, area_2 = args[1], args[2]
                    if area_1 <= self.id <= area_2:
                        return client
            raise AreaError.ClientNotFound

        def get_clock_period(self) -> str:
            """
            Return the period of a clock initiated by the user with the smallest ID that involves
            the current area.
            If no such users exists, return an empty string.

            Returns
            -------
            str
                Period name.
            """

            try:
                client = self.get_clock_creator()
            except AreaError.ClientNotFound:
                return ''
            else:
                period = self.server.tasker.get_task_attr(client, ['as_day_cycle'], 'period')
                return period

        def unlock(self):
            """
            Unlock the area so that non-authorized players may now join.
            """

            self.is_locked = False
            if not self.is_gmlocked and not self.is_modlocked:
                self.invite_list = {}

        def gmunlock(self):
            """
            Unlock the area if it had a GM lock so that non-authorized players may now join.
            """

            self.is_gmlocked = False
            self.is_locked = False
            if not self.is_modlocked:
                self.invite_list = {}

        def modunlock(self):
            """
            Unlock the area if it had a mod lock so that non-authorized players may now join.
            """

            self.is_modlocked = False
            self.is_gmlocked = False
            self.is_locked = False
            self.invite_list = {}

        @property
        def in_zone(self):
            """
            Declarator for a public in_zone attribute.
            """

            return self._in_zone

        @in_zone.setter
        def in_zone(self, new_zone_value: ZoneManager.Zone):
            """
            Set the in_zone parameter to the given one

            Parameters
            ----------
            new_zone_value: ZoneManager.Zone or None
                New zone the area belongs to.

            Raises
            ------
            AreaError:
                If the area was not part of a zone and new_zone_value is None or,
                if the area was part of a zone and new_zone_value is not None.
            """

            if new_zone_value is None and self._in_zone is None:
                raise AreaError('This area is already not part of a zone.')
            if new_zone_value is not None and self._in_zone is not None:
                raise AreaError('This area is already part of a zone.')

            self._in_zone = new_zone_value

        def destroy(self):
            """
            Emit destruction signal.

            Returns
            -------
            None.

            """

            self.publisher.publish('area_destroyed', dict())

        def __repr__(self):
            """
            Return a string representation of the area.

            The string follows the convention 'A::AreaID:AreaName:ClientsInArea'
            """

            return 'A::{}:{}:{}'.format(self.id, self.name, len(self.clients))

    def __init__(self, server: TsuserverDR):
        """
        Create an area manager object.

        Parameters
        ----------
        server: TsuserverDR
            The server this area belongs to.
        """

        self.server = server
        self.areas = []
        self.area_names = set()
        self.publisher = Publisher(self)
        self.load_areas()

    def load_areas(self, area_list_file: str = 'config/areas.yaml'):
        """
        Load an area list.

        Parameters
        ----------
        area_list_file: str, optional
            Location of the area list to load. Defaults to 'config/areas.yaml'.

        Raises
        ------
        AreaError
            If any one of the following conditions are met:
            * An area has no 'area' or no 'background' tag.
            * An area uses the deprecated 'sound_proof' tag.
            * Two areas have the same name.
            * An area parameter was left deliberately blank as opposed to fully erased.
            * An area has a passage to an undefined area.

        FileNotFound
            If the area list could not be found.
        """

        areas = ValidateAreas().validate(area_list_file, extra_parameters={
            'server_character_list': self.server.char_list,
            'server_default_area_description': self.server.config['default_area_description']
            })

        # Now we are ready to create the areas
        temp_areas = list()
        for (i, area_item) in enumerate(areas):
            temp_areas.append(self.Area(i, self.server, area_item))

        old_areas = self.areas
        self.areas = temp_areas
        self.area_names = [area.name for area in self.areas]

        # Only once all areas have been created, actually set the corresponding values
        # Helps avoiding junk area lists if there was an error
        # But first, remove all zones
        backup_zones = self.server.zone_manager.get_zones()
        for (zone_id, zone) in backup_zones.items():
            self.server.zone_manager.delete_zone(zone_id)
            for client in zone.get_watchers():
                client.send_ooc('Your zone has been automatically deleted due to an area list '
                                'load.')

        # And end all existing day cycles
        for client in self.server.get_clients():
            try:
                client.server.tasker.remove_task(client, ['as_day_cycle'])
            except KeyError:
                pass

        # And remove all global IC and global IC prefixes
        for client in self.server.get_clients():
            if client.multi_ic:
                client.send_ooc('Due to an area list reload, your global IC was turned off. You '
                                'may turn it on again manually.')
                client.multi_ic = None
            if client.multi_ic_pre:
                client.send_ooc('Due to an area list reload, your global IC prefix was removed. '
                                'You may set it again manually.')
                client.multi_ic_pre = ''

        # And do other tasks associated with areas reloading
        self.publisher.publish('areas_loaded', dict())

        # If the default area ID is now past the number of available areas, reset it back to zero
        if self.server.default_area >= len(self.areas):
            self.server.default_area = 0

        for area in old_areas:
            # Decide whether the area still exists or not
            try:
                new_area = self.get_area_by_name(area.name)
                remains = True
            except AreaError:
                new_area = self.default_area()
                remains = False

            # Move existing clients to new corresponding area (or to default area if their previous
            # area no longer exists).
            for client in area.clients.copy():
                # Check if current char is available
                if new_area.is_char_available(client.char_id):
                    new_char_id = client.char_id
                else:
                    try:
                        new_char_id = new_area.get_rand_avail_char_id()
                    except AreaError:
                        new_char_id = -1

                if remains:
                    message = 'Area list reload. Moving you to the new {}.'
                else:
                    message = ('Area list reload. Your previous area no longer exists. Moving you '
                               'to the server default area {}.')

                client.send_ooc(message.format(new_area.name))
                client.change_area(new_area, ignore_checks=True, change_to=new_char_id,
                                   ignore_notifications=True)

            # Move parties (independently)
            for party in area.parties.copy():
                party.area = new_area
                new_area.add_party(party)

        # Once that is done, indicate old areas to emit destruction signals
        # This is done separately
        for area in old_areas:
            area.destroy()

        # Update the server's area list only once everything is successful
        self.server.old_area_list = self.server.area_list
        self.server.area_list = area_list_file

    def default_area(self) -> AreaManager.Area:
        """
        Return the Area object corresponding to the server's default area.
        """

        return self.areas[self.server.default_area]

    def get_area_by_name(self, name: str) -> AreaManager.Area:
        """
        Return the Area object corresponding to the area that has the given name.

        Parameters
        ----------
        name: str
            Area name to look for.

        Returns
        -------
        AreaManager.Area
            Area.

        Raises
        ------
        AreaError
            If no area has the given name.
        """

        for area in self.areas:
            if area.name == name:
                return area
        raise AreaError('Area not found.')

    def get_area_by_id(self, area_id: int) -> AreaManager.Area:
        """
        Return the Area object corresponding to the area that has the given ID.

        Parameters
        ----------
        area_id: int
            Area ID to look for.

        Returns
        -------
        AreaManager.Area
            Area.

        Raises
        ------
        AreaError
            If no area has the given ID.
        """

        for area in self.areas:
            if area.id == area_id:
                return area
        raise AreaError('Area not found.')

    def get_areas_in_range(self, area1: AreaManager.Area,
                           area2: AreaManager.Area) -> Set[AreaManager.Area]:
        """
        Return all areas whose ID is at least area1's and at most area2's (both inclusive).
        If both areas have the same ID, return just the given area.
        If area2's ID is smaller than area1's, return the empty set.

        Parameters
        ----------
        area1: self.Area
            Area whose ID will be the lower bound.
        area2: self.Area
            Area whose ID will be the upper bound

        Returns
        ------
        set of self.Area
            All areas in `self.areas` that satisfy area1.id <= area.id <= area2.id
        """

        return {self.get_area_by_id(i) for i in range(area1.id, area2.id+1)}

    def change_passage_lock(self, client: ClientManager.Client,
                            areas: List[AreaManager.Area],
                            bilock: bool = False,
                            change_passage_visibility: bool = False):
        now_reachable = []
        num_areas = 2 if bilock else 1

        # First check if the player should be able to change the passage at all
        for i in range(num_areas):
            # First check if it is the case a non-authorized use is trying to change passages to
            # areas that do not allow their passages to be modified
            if not areas[i].change_reachability_allowed and not client.is_staff():
                raise AreaError('You must be authorized to change passages in area {}.'
                                .format(areas[i].name))

            # And make sure that non-authorized users cannot create passages they cannot see
            if ((not areas[1-i].name in areas[i].reachable_areas) and
                not (client.is_staff() or areas[1-i].name in areas[i].visible_reachable_areas)):
                raise AreaError('You must be authorized to create a new passage from {} to '
                                '{}.'.format(areas[i].name, areas[1-i].name))

        # If we are at this point, we are committed to changing the passage locks
        for i in range(num_areas):
            if areas[1-i].name in areas[i].reachable_areas:  # Case removing a passage
                now_reachable.append(False)
                areas[i].reachable_areas -= {areas[1-i].name}
                if change_passage_visibility:
                    areas[i].visible_reachable_areas -= {areas[1-i].name}
            else:  # Case creating a passage
                now_reachable.append(True)
                areas[i].reachable_areas.add(areas[1-i].name)
                if change_passage_visibility:
                    areas[i].visible_reachable_areas.add(areas[1-i].name)

            for client in areas[i].clients:
                client.reload_music_list()

        return now_reachable


import sys
import pathlib
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from tqdm.auto import tqdm, trange
tqdm.pandas(desc='Progress')

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence

from custom_ml import ConcatPoolingGRUAdaptive, SentimentDataset, collate_fn

import ignite
from ignite.engine import Engine, Events
from ignite.metrics import Accuracy, Loss, RunningAverage
from ignite.handlers import ModelCheckpoint, EarlyStopping
from ignite.contrib.handlers import ProgressBar

from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# import warnings
# warnings.filterwarnings('ignore')

# from IPython.core.interactiveshell import InteractiveShell
# InteractiveShell.ast_node_interactivity='all'

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

print('Python version:',sys.version)
print('Pandas version:',pd.__version__)
print('Pytorch version:', torch.__version__)
print('Ignite version:', ignite.__version__)

data_root = pathlib.Path('./data')

# load csv in pandas dataframe
df = pd.read_csv(data_root / 'cleaned_tweets.csv', error_bad_lines=False)

# split the data into train and validation
train_df, val_df = train_test_split(df, test_size=0.2, random_state=42, stratify=df[['Sentiment']])
train_df, val_df = train_df.reset_index(drop=True), val_df.reset_index(drop=True)

print(train_df.shape, val_df.shape)

# ******************** #
# * Training Dataset * #
# ******************** #
vocab_size = 100000
train_ds = SentimentDataset(train_df, max_vocab_size=vocab_size)
train_ds.save_vocabulary()

# ********************** #
# * Validation dataset * #
# ********************** #

val_ds = SentimentDataset(val_df, word2idx=train_ds.word2idx, idx2word=train_ds.idx2word)

batch_size = 1024

# *************** #
# * Dataloaders * #
# *************** #

train_dl = DataLoader(train_ds, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
val_dl = DataLoader(val_ds, batch_size=batch_size, collate_fn=collate_fn)

#####################
# Set up the models #
#####################

# (vocab_size + 2) is because of pad and unk added to the vocab
model_vocab_size = vocab_size + 2
embedding_dim = 100
rnn_hidden = 256
n_out = 2

model = ConcatPoolingGRUAdaptive(model_vocab_size, embedding_dim, rnn_hidden, n_out).to(device)
optimizer = optim.Adam(model.parameters(), 1e-3)
loss_fn = F.nll_loss

# ****************************************************** #
# *              Ignite training Callbacks             * #
# Ignite is all about callbacks.                         #
# Training and evaluation is defined separately.         #
# You can define your single custom training             #
# and evaluator loop and add them to Engine.             #
# Add loss and accuracy to the trainer and evaluator.    #
# Finally define early stopping and modelcheckpoint      #
# ****************************************************** #
##############################################
# Define single training and validation loop #
##############################################

def process_function(engine, batch):
    """Single training loop to be attached to trainer Engine
    """
    model.train()
    optimizer.zero_grad()
    x, y, lens = batch
    x, y = x.to(device), y.to(device)
    y_pred = model(x, lens)
    loss = loss_fn(y_pred, y)
    loss.backward()
    optimizer.step()
    return loss.item(), torch.max(y_pred, dim=1)[1], y

def eval_function(engine, batch):
    """Single evaluator loop to be attached to trainer and evaluator Engine
    """
    model.eval()
    with torch.no_grad():
        x, y, lens = batch
        x, y = x.to(device), y.to(device)
        y_pred = model(x, lens)
        return y_pred, y

trainer = Engine(process_function)
train_evaluator = Engine(eval_function)
validation_evaluator = Engine(eval_function)

################################################################
# Add metrics (Loss and Accuracy) to the trainer and evaluator #
################################################################
def max_output_transform(output):
    """It convers the predicted ouput probabilties to indexes for accuracy calculation
    """
    y_pred, y = output
    return torch.max(y_pred, dim=1)[1], y

# attach running loss (will be displayed in progess bar)
RunningAverage(output_transform=lambda x: x[0]).attach(trainer, 'loss')

# attach running accuracy (will be displayed in progess bar)
RunningAverage(Accuracy(output_transform=lambda x: [x[1], x[2]])).attach(trainer, 'acc')

# attach accuracy and loss to train_evaluator
Accuracy(output_transform=max_output_transform).attach(train_evaluator, 'accuracy')
Loss(loss_fn).attach(train_evaluator, 'bce')

# attach accuracy and loss to validation_evaluator
Accuracy(output_transform=max_output_transform).attach(validation_evaluator, 'accuracy')
Loss(loss_fn).attach(validation_evaluator, 'bce')
#############################################
# Report progress through tqdm progress bar #
#############################################
pbar = ProgressBar(persist=True, bar_format="")
pbar.attach(trainer, ['loss', 'acc'])

# Log after each EPOCH
@trainer.on(Events.EPOCH_COMPLETED)
def log_training_results(engine):
    """This function will run after each epoch and
       report the training loss and accuracy (defined above)
    """
    train_evaluator.run(train_dl)
    metrics = train_evaluator.state.metrics
    avg_accuracy = metrics['accuracy']
    avg_bce = metrics['bce']
    pbar.log_message(
        f'Training Results - Epoch: {engine.state.epoch}  Avg accuracy: {avg_accuracy:.4f} Avg loss: {avg_bce:.4f}')

@trainer.on(Events.EPOCH_COMPLETED)
def log_validation_results(engine):
    """This function will run after each epoch and
       report the validation loss and accuracy (defined above)
    """
    validation_evaluator.run(val_dl)
    metrics = validation_evaluator.state.metrics
    avg_accuracy = metrics['accuracy']
    avg_bce = metrics['bce']
    pbar.log_message(
        f'Validation Results - Epoch: {engine.state.epoch}  Avg accuracy: {avg_accuracy:.4f} Avg loss: {avg_bce:.4f}')
    pbar.n = pbar.last_print_n = 0

###################################################
# Callback for Early stopping and ModelCheckpoint #
###################################################
def score_function(engine):
    """EarlyStopping will call this function to check if score improved
    """
    val_loss = engine.state.metrics['bce']
    return -val_loss

early_stopping = EarlyStopping(patience=3, score_function=score_function, trainer=trainer)
validation_evaluator.add_event_handler(Events.COMPLETED, early_stopping)

checkpointer = ModelCheckpoint(
    './models',
    'text_gru_concat',
    save_interval=1,
    n_saved=1,
    create_dir=True,
    save_as_state_dict=True)

trainer.add_event_handler(Events.EPOCH_COMPLETED, checkpointer, {'sentiment': model})

trainer.run(train_dl, max_epochs=10)