"""
[summary]
"""
import simpy
import math
import datetime
import itertools
from collections import defaultdict

try:
    # Python 3.8
    from functools import cached_property
except ImportError:
    # Previous version of python need to install cached_property
    from cached_property import cached_property

from covid19sim.utils import compute_distance, _get_random_area, _draw_random_discreet_gaussian, calculate_average_infectiousness
from covid19sim.track import Tracker
from covid19sim.interventions import *
from covid19sim.frozen.utils import update_uid

class Env(simpy.Environment):
    """
    Custom simpy.Environment
    """

    def __init__(self, initial_timestamp):
        """
        Args:
            initial_timestamp (datetime.datetime): The environment's initial timestamp
        """
        self.initial_timestamp = datetime.datetime.combine(initial_timestamp.date(),
                                                           datetime.time())
        self.ts_initial = int(self.initial_timestamp.timestamp())
        super().__init__(self.ts_initial)

    @property
    def timestamp(self):
        """
        Returns:
            datetime.datetime: Current date.
        """
        #
        ##
        ## The following is preferable, but not equivalent to the initial
        ## version, because timedelta ignores Daylight Saving Time.
        ##
        #
        #return datetime.datetime.fromtimestamp(int(self.now))
        #
        return self.initial_timestamp + datetime.timedelta(
            seconds=self.now-self.ts_initial)

    def minutes(self):
        """
        Returns:
            int: Current timestamp minute
        """
        return self.timestamp.minute

    def hour_of_day(self):
        """
        Returns:
            int: Current timestamp hour
        """
        return self.timestamp.hour

    def day_of_week(self):
        """
        Returns:
            int: Current timestamp day of the week
        """
        return self.timestamp.weekday()

    def is_weekend(self):
        """
        Returns:
            bool: Current timestamp day is a weekend day
        """
        return self.day_of_week() >= 5

    def time_of_day(self):
        """
        Time of day in iso format
        datetime(2020, 2, 28, 0, 0) => '2020-02-28T00:00:00'

        Returns:
            str: iso string representing current timestamp
        """
        return self.timestamp.isoformat()

class City:
    """
    City
    """

    def __init__(self, env, n_people, init_percent_sick, rng, x_range, y_range, Human, conf):
        """
        Args:
            env (simpy.Environment): [description]
            n_people (int): Number of people in the city
            init_percent_sick: % of population to be infected on day 0
            rng (np.random.RandomState): Random number generator
            x_range (tuple): (min_x, max_x)
            y_range (tuple): (min_y, max_y)
            Human (covid19.simulator.Human): Class for the city's human instances
            conf (dict): yaml configuration of the experiment
        """
        self.conf = conf
        self.env = env
        self.rng = rng
        self.x_range = x_range
        self.y_range = y_range
        self.total_area = (x_range[1] - x_range[0]) * (y_range[1] - y_range[0])
        self.n_people = n_people
        self.init_percent_sick = init_percent_sick
        self.last_date_to_check_tests = self.env.timestamp.date()
        self.test_count_today = defaultdict(int)
        self.test_type_preference = list(zip(*sorted(self.conf.get("TEST_TYPES").items(), key=lambda x:x[1]['preference'])))[0]
        print("Initializing locations ...")
        self.initialize_locations()

        self.humans = []
        self.households = OrderedSet()
        print("Initializing humans ...")
        self.initialize_humans(Human)

        if self.conf.get('COLLECT_LOGS'):
            self.log_static_info()

        print("Computing their preferences")
        self._compute_preferences()
        self.tracker = Tracker(env, self)
        # self.tracker.track_initialized_covid_params(self.humans)

        self.intervention = None

    @property
    def start_time(self):
        return datetime.datetime.fromtimestamp(self.env.ts_initial)

    def create_location(self, specs, type, name, area=None):
        """
        Create a location instance based on `type`

        Specs is a dict like:
        {
            "n" : (int) number of such locations,
            "area": (float) locations' typical area,
            "social_contact_factor": (float(0:1)) how much people are close to each other
                see contamination_probability(),
            "surface_prob": [0.1, 0.1, 0.3, 0.2, 0.3], distribution over types of surfaces
                in that location
            "rnd_capacity": (tuple, optional) Either None or a tuple of ints (min, max)
            describing the args of np.random.randint,
        }

        Args:
            specs (dict): location's parameters
            type (str): "household" and "senior_residency" create a Household instance,
                "hospital" creates a Hospital, other strings create a generic Location
            name (str): Location's name, created as `type:name`
            area (float, optional): Location's area. Defaults to None.

        Returns:
            Location | Household | Hospital: new location instance
        """
        _cls = Location
        if type in ['household', 'senior_residency']:
            _cls = Household
        if type == 'hospital':
            _cls = Hospital

        return   _cls(
                        env=self.env,
                        rng=self.rng,
                        name=f"{type}:{name}",
                        location_type=type,
                        lat=self.rng.randint(*self.x_range),
                        lon=self.rng.randint(*self.y_range),
                        area=area,
                        social_contact_factor=specs['social_contact_factor'],
                        capacity= None if not specs['rnd_capacity'] else self.rng.randint(*specs['rnd_capacity']),
                        surface_prob = specs['surface_prob']
                        )

    @property
    def tests_available(self):
        """
        Returns:
            bool: tests are available
        """
        if self.last_date_to_check_tests != self.env.timestamp.date():
            self.last_date_to_check_tests = self.env.timestamp.date()
            for k in self.test_count_today.keys():
                self.test_count_today[k] = 0
        return any(self.test_count_today[test_type] < self.conf.get("TEST_TYPES")[test_type]['capacity'] for test_type in self.test_type_preference)

    def get_available_test(self):
        """
        Returns a test_type: the first type that is available according to preference
        hierarchy (TEST_TYPES[test_type]['preference']).

        See TEST_TYPES in config.py

        Returns:
            str: available test_type
        """
        for test_type in self.test_type_preference:
            if self.test_count_today[test_type] < self.conf.get("TEST_TYPES")[test_type]['capacity']:
                self.test_count_today[test_type] += 1
                return test_type

    def initialize_locations(self):
        """
        Create locations according to config.py / LOCATION_DISTRIBUTION.
        The City instance will have attributes <location_type>s = list(location(*args))
        """
        for location, specs in self.conf.get("LOCATION_DISTRIBUTION").items():
            if location in ['household']:
                continue

            n = math.ceil(self.n_people/specs["n"])
            area = _get_random_area(n, specs['area'] * self.total_area, self.rng)
            locs = [self.create_location(specs, location, i, area[i]) for i in range(n)]
            setattr(self, f"{location}s", locs)

    def initialize_humans(self, Human):
        """
        allocate humans to houses such that (unsolved)
        1. average number of residents in a house is (approx.) 2.6
        2. not all residents are below 15 years of age
        3. age occupancy distribution follows HUMAN_DSITRIBUTION.residence_preference.house_size

        current implementation is an approximate heuristic

        Args:
            Human (Class): Class for the city's human instances
        """

        # make humans
        count_humans = 0
        house_allocations = {2:[], 3:[], 4:[], 5:[]}
        n_houses = 0

        # initial infections
        init_infected = math.ceil(self.init_percent_sick * self.n_people)
        chosen_infected = set(self.rng.choice(self.n_people, init_infected, replace=False).tolist())

        # app users
        all_has_app = self.conf.get('P_HAS_APP') < 0
        n_apps = self.conf.get('P_HAS_APP') * self.n_people if self.conf.get('P_HAS_APP') > 0 else self.n_people
        n_apps_per_age = {
            k:math.ceil(v * n_apps)
            for k,v in self.conf.get("APP_USERS_FRACTION_BY_AGE").items()
        }

        for age_bin, specs in self.conf.get("HUMAN_DISTRIBUTION").items():
            n = math.ceil(specs['p'] * self.n_people)
            ages = self.rng.randint(*age_bin, size=n)

            senior_residency_preference = specs['residence_preference']['senior_residency']

            professions = ['healthcare', 'school', 'others', 'retired']
            p = [specs['profession_profile'][x] for x in professions]
            profession = self.rng.choice(professions, p=p, size=n)

            # select who should have app based on self.conf's APP_USERS_FRACTION_BY_AGE
            chosen_app_user_bin = []
            for my_age in ages:
                for x, frac in self.conf.get("APP_USERS_FRACTION_BY_AGE").items():
                    if x[0] <= my_age <= x[1]:
                        chosen_app_user_bin.append(x)
                        break

            for i in range(n):
                count_humans += 1
                age = ages[i]

                # should the person has an app?
                current_app_bin = chosen_app_user_bin[i]
                if n_apps_per_age[current_app_bin] > 0:
                    has_app = True
                    n_apps_per_age[current_app_bin] -= 1
                else:
                    has_app = False

                # residence
                res = None
                if self.rng.random() < senior_residency_preference:
                    res = self.rng.choice(self.senior_residencys)
                # workplace
                if profession[i] == "healthcare":
                    workplace = self.rng.choice(self.hospitals + self.senior_residencys)
                elif profession[i] == 'school':
                    workplace = self.rng.choice(self.schools)
                elif profession[i] == 'others':
                    type_of_workplace = self.rng.choice(
                        [0,1,2],
                        p=self.conf.get("OTHERS_WORKPLACE_CHOICE"),
                        size=1
                    ).item()
                    type_of_workplace = [self.workplaces, self.stores, self.miscs][type_of_workplace]
                    workplace = self.rng.choice(type_of_workplace)
                else:
                    workplace = res

                self.humans.append(Human(
                        env=self.env,
                        city=self,
                        rng=self.rng,
                        has_app=has_app or all_has_app,
                        name=count_humans,
                        age=age,
                        household=res,
                        workplace=workplace,
                        profession=profession[i],
                        rho=self.conf.get("RHO"),
                        gamma=self.conf.get("GAMMA"),
                        infection_timestamp=self.start_time if count_humans - 1 in chosen_infected else None,
                        conf=self.conf
                        )
                    )

        # assign houses
        # stores tuples - (location, current number of residents, maximum number of residents allowed)
        remaining_houses = []
        for human in self.humans:
            if human.household is not None:
                continue
            if len(remaining_houses) == 0:
                cap = self.rng.choice(range(1,6), p=self.conf.get("HOUSE_SIZE_PREFERENCE"), size=1)
                x = self.create_location(
                    self.conf.get("LOCATION_DISTRIBUTION")['household'],
                    'household',
                    len(self.households)
                )

                remaining_houses.append((x, cap))

            # get_best_match
            res = None
            for  c, (house, n_vacancy) in enumerate(remaining_houses):
                new_avg_age = (human.age + sum(x.age for x in house.residents))/(len(house.residents) + 1)
                if new_avg_age > self.conf.get("MIN_AVG_HOUSE_AGE"):
                    res = house
                    n_vacancy -= 1
                    if n_vacancy == 0:
                        remaining_houses = remaining_houses[:c] + remaining_houses[c+1:]
                    break

            if res is None:
                for i, (l,u) in enumerate(self.conf.get("HUMAN_DISTRIBUTION").keys()):
                    if l <= human.age < u:
                        bin = (l,u)
                        break

                house_size_preference = self.conf.get("HUMAN_DISTRIBUTION")[(l,u)]['residence_preference']['house_size']
                cap = self.rng.choice(range(1,6), p=house_size_preference, size=1)
                res = self.create_location(
                    self.conf.get("LOCATION_DISTRIBUTION")['household'],
                    'household',
                    len(self.households)
                )
                if cap - 1 > 0:
                    remaining_houses.append((res, cap-1))

            # FIXME: there is some circular reference here
            res.residents.append(human)
            human.assign_household(res)
            self.households.add(res)

        # assign area to house
        area = _get_random_area(
            len(self.households),
            self.conf.get("LOCATION_DISTRIBUTION")['household']['area'] * self.total_area,
            self.rng
        )

        for i,house in enumerate(self.households):
            house.area = area[i]

    @cached_property
    def hd(self):
        """This lazy cached_property allows for easy O(1) access of humans for message passing"""
        return {human.name: human for human in self.humans}

    def log_static_info(self):
        """
        Logs events for all humans in the city
        """
        for h in self.humans:
            Event.log_static_info(self, h, self.env.timestamp)

    @property
    def events(self):
        """
        Get all events of all humans in the city

        Returns:
            list: all of everyone's events
        """
        return list(itertools.chain(*[h.events for h in self.humans]))

    def events_slice(self, begin, end):
        """
        Get all sliced events of all humans in the city

        Args:
            begin (datetime.datetime): minimum time of events
            end (int): maximum time of events

        Returns:
            list: The list each human's events, restricted to a slice
        """
        return list(itertools.chain(*[h.events_slice(begin, end) for h in self.humans]))

    def pull_events_slice(self, end):
        """
        Get the list of all human's events before `end`.
        /!\ Modifies each human's events

        Args:
            end (datetime.datetime): maximum time of pulled events

        Returns:
            list: All the events which occured before `end`
        """
        return list(itertools.chain(*[h.pull_events_slice(end) for h in self.humans]))

    def _compute_preferences(self):
        """
        Compute preferred distribution of each human for park, stores, etc.
        /!\ Modifies each human's stores_preferences and parks_preferences
        """
        for h in self.humans:
            h.stores_preferences = [(compute_distance(h.household, s) + 1e-1) ** -1 for s in self.stores]
            h.parks_preferences = [(compute_distance(h.household, s) + 1e-1) ** -1 for s in self.parks]

    def run(self, duration, outfile):
        """
        Run the City.
        Several daily tasks take place here.
        Examples include - (1) modifying behavior of humans based on an intervention
        (2) gathering daily statistics on humans

        Args:
            duration (int): duration of a step, in seconds.
            outfile (str): may be None, the run's output file to write to

        Yields:
            simpy.Timeout
        """
        self.current_day = 0
        humans_notified = False
        tmp_M = self.conf.get("GLOBAL_MOBILITY_SCALING_FACTOR")
        self.conf["GLOBAL_MOBILITY_SCALING_FACTOR"] = 1
        while True:
            # Notify humans to follow interventions on intervention day
            if self.current_day == self.conf.get('INTERVENTION_DAY') and not humans_notified:
                self.intervention = get_intervention(
                    key=self.conf.get('INTERVENTION'),
                    RISK_MODEL=self.conf.get('RISK_MODEL'),
                    TRACING_ORDER=self.conf.get('TRACING_ORDER'),
                    TRACE_SYMPTOMS=self.conf.get('TRACE_SYMPTOMS'),
                    TRACE_RISK_UPDATE=self.conf.get('TRACE_RISK_UPDATE'),
                    SHOULD_MODIFY_BEHAVIOR=self.conf.get('SHOULD_MODIFY_BEHAVIOR'),
                    MASKS_SUPPLY=self.conf.get("MASKS_SUPPLY")
                )

                _ = [h.notify(self.intervention) for h in self.humans]
                print(self.intervention)
                self.conf["GLOBAL_MOBILITY_SCALING_FACTOR"] = tmp_M
                if self.conf.get('COLLECT_TRAINING_DATA'):
                    print("naive risk calculation without changing behavior... Humans notified!")

                humans_notified = True

            # iterate over humans, and if it's their timeslot, then update their infectionsness, symptoms, and message info
            for human in self.humans:
                # if it's your time to update,
                if self.env.timestamp.hour not in human.time_slots:
                    continue

                # And you haven't updated today
                if human.last_date.get('symptoms_updated') == self.env.timestamp.date():
                    continue

                human.last_date['symptoms_updated'] = self.env.timestamp.date()
                human.update_symptoms()
                human.update_reported_symptoms()
                human.update_risk(symptoms=human.symptoms)
                human.infectiousnesses.appendleft(calculate_average_infectiousness(human))

                if self.conf.get('COLLECT_LOGS'):
                    Event.log_daily(human, human.env.timestamp)

                self.tracker.track_symptoms(human)

                # keep only past N_DAYS contacts
                if human.tracing:
                    for type_contacts in ['n_contacts_tested_positive', 'n_contacts_symptoms', \
                                          'n_risk_increased', 'n_risk_decreased', "n_risk_mag_decreased",
                                          "n_risk_mag_increased"]:
                        for order in human.message_info[type_contacts]:
                            if len(human.message_info[type_contacts][order]) > self.conf.get('TRACING_N_DAYS_HISTORY'):
                                human.message_info[type_contacts][order] = human.message_info[type_contacts][order][1:]
                            human.message_info[type_contacts][order].append(0)

            if isinstance(self.intervention, Tracing):
                self.intervention.update_human_risks(
                    city=self,
                    data_path=outfile,
                    conf=self.conf,
                )
                self.tracker.track_risk_attributes(self.humans)

            # Let the hour pass
            yield self.env.timeout(int(duration))

            # increment the day / update uids if we start the timeslot 0
            if self.env.timestamp.hour == 0 and self.env.timestamp != self.env.initial_timestamp:
                # TODO: this is an assumption which will break in reality, instead of updating once per day everyone at the same time, it should be throughout the day
                for human in self.humans:
                    human.uid = update_uid(human.uid, human.rng)
                self.current_day += 1
                self.tracker.increment_day()

class Location(simpy.Resource):
    """
    Class representing generic locations used in the simulator
    """

    def __init__(self, env, rng, area, name, location_type, lat, lon,
            social_contact_factor, capacity, surface_prob):
        """
        Locations are created with city.create_location(), not instantiated directly

        Args:
            env (covid19sim.Env): Shared environment
            rng (np.random.RandomState): Random number generator
            area (float): Area of the location
            name (str): The location's name
            location_type (str): Location's type, see
            lat (float): Location's latitude
            lon (float): Location's longitude
            social_contact_factor (float): how much people are close to each other
                see contamination_probability() (this scales the contamination pbty)
            capacity (int): Daily intake capacity for the location (infinity if None).
            surface_prob (float): distribution of surface types in the Location. As
                different surface types have different contamination probabilities
                and virus "survival" durations, this will influence the contamination
                of humans at this location.
                Surfaces: aerosol, copper, cardboard, steel, plastic
        """

        if capacity is None:
            capacity = simpy.core.Infinity

        super().__init__(env, capacity)
        self.humans = OrderedSet() #OrderedSet instead of set for determinism when iterating
        self.name = name
        self.rng = rng
        self.lat = lat
        self.lon = lon
        self.area = area
        self.location_type = location_type
        self.social_contact_factor = social_contact_factor
        self.env = env
        self.contamination_timestamp = datetime.datetime.min
        self.contaminated_surface_probability = surface_prob
        self.max_day_contamination = 0
        self.is_open_for_business = True

    def infectious_human(self):
        """
        Returns:
            bool: Is there an infectious human currently at that location
        """
        return any([h.is_infectious for h in self.humans])

    def __repr__(self):
        """
        Returns:
            str: Representation of the Location
        """
        return f"{self.name} - occ:{len(self.humans)}/{self.capacity} - I:{self.is_contaminated}"

    def add_human(self, human):
        """
        Adds a human instance to the OrderedSet of humans at the location.
        If they are infectious, then location.contamination_timestamp is set to the
        env's timestamp and the duration of this contamination is set
        (location.max_day_contamination) according to the distribution of surfaces
        (location.contaminated_surface_probability) and the survival of the virus
        per surface type (MAX_DAYS_CONTAMINATION)

        Args:
            human (covid19sim.simulator.Human): The human to add.
        """
        self.humans.add(human)
        if human.is_infectious:
            self.contamination_timestamp = self.env.timestamp
            rnd_surface = float(self.rng.choice(
                    a=human.conf.get("MAX_DAYS_CONTAMINATION"),
                    size=1,
                    p=self.contaminated_surface_probability
            ))
            self.max_day_contamination = max(self.max_day_contamination, rnd_surface)

    def remove_human(self, human):
        """
        Remove a given human from location.human
        /!\ Human is not returned

        Args:
            human (covid19sim.simulator.Human): The human to remove
        """
        self.humans.remove(human)

    @property
    def is_contaminated(self):
        """
        Is the location currently contaminated? This depends on the moment
        when it got contaminated (see add_human()), current time and the
        duration of the contamination (location.max_day_contamination)

        Returns:
            bool: Is the place currently contaminating?
        """
        return self.env.timestamp - self.contamination_timestamp <= datetime.timedelta(days=self.max_day_contamination)

    @property
    def contamination_probability(self):
        """
        Contamination depends on the time the virus has been sitting on a given surface
        (location.max_day_contamination) and is linearly decayed over time.
        Then it is scaled by location.social_contact_factor

        If not location.is_contaminated, return 0.0

        Returns:
            float: probability that a human is contaminated when going to this location.
        """
        if self.is_contaminated:
            lag = (self.env.timestamp - self.contamination_timestamp)
            lag /= datetime.timedelta(days=1)
            p_infection = 1 - lag / self.max_day_contamination # linear decay; &envrionmental_contamination
            return self.social_contact_factor * p_infection
        return 0.0

    def __hash__(self):
        """
        Hash of the location is the hash of its name

        Returns:
            int: hash
        """
        return hash(self.name)

    def serialize(self):
        """
        This function serializes the location object by deleting
        non-serializable keys

        Returns:
            dict: serialized location
        """
        s = self.__dict__
        if s.get('env'):
            del s['env']
        if s.get('rng'):
            del s['rng']
        if s.get('_env'):
            del s['_env']
        if s.get('contamination_timestamp'):
            del s['contamination_timestamp']
        if s.get('residents'):
            del s['residents']
        if s.get('humans'):
            del s['humans']
        return s

class Household(Location):
    """
    Household location class, inheriting from covid19sim.base.Location
    """
    def __init__(self, **kwargs):
        """
        Args:
            kwargs (dict): all the args necessary for a Location's init
        """
        super(Household, self).__init__(**kwargs)
        self.residents = []

class Hospital(Location):
    """
    Hospital location class, inheriting from covid19sim.base.Location
    """
    ICU_AREA = 0.10
    ICU_CAPACITY = 0.10
    def __init__(self, **kwargs):
        """
        Create the Hospital and its ICU

        Args:
            kwargs (dict): all the args necessary for a Location's init
        """
        env = kwargs.get('env')
        rng = kwargs.get('rng')
        capacity = kwargs.get('capacity')
        name = kwargs.get("name")
        lat = kwargs.get('lat')
        lon = kwargs.get('lon')
        area = kwargs.get('area')
        surface_prob = kwargs.get('surface_prob')
        social_contact_factor = kwargs.get('social_contact_factor')

        super(Hospital, self).__init__( env=env,
                                        rng=rng,
                                        area=area * (1-self.ICU_AREA),
                                        name=name,
                                        location_type="hospital",
                                        lat=lat,
                                        lon=lon,
                                        social_contact_factor=social_contact_factor,
                                        capacity=int(capacity* (1- self.ICU_CAPACITY)),
                                        surface_prob=surface_prob,
                                        )
        self.location_contamination = 1
        self.icu = ICU( env=env,
                        rng=rng,
                        area=area * (self.ICU_AREA),
                        name=f"{name}-icu",
                        location_type="hospital-icu",
                        lat=lat,
                        lon=lon,
                        social_contact_factor=social_contact_factor,
                        capacity=int(capacity* (self.ICU_CAPACITY)),
                        surface_prob=surface_prob,
                        )

    def add_human(self, human):
        """
        Add a human to the Hospital's OrderedSet through the Location's
        default add_human() method + set the human's obs_hospitalized attribute
        is set to True

        Args:
            human (covid19sim.simulator.Human): human to add
        """
        human.obs_hospitalized = True
        super().add_human(human)

    def remove_human(self, human):
        """
        Remove a human from the Hospital's Ordered set.
        On top of Location.remove_human(), the human's obs_hospitalized attribute is
        set to False

        Args:
            human (covid19sim.simulator.Human): human to remove
        """
        human.obs_hospitalized = False
        super().remove_human(human)

class ICU(Location):
    """
    Hospital location class, inheriting from covid19sim.base.Location
    """
    def __init__(self, **kwargs):
        """
        Create a Hospital's ICU Location

        Args:
            kwargs (dict): all the args necessary for a Location's init
        """
        super().__init__(**kwargs)

    def add_human(self, human):
        """
        Add a human to the ICU's OrderedSet through the Location's
        default add_human() method + set the human's obs_hospitalized and
        obs_in_icu attributes are set to True

        Args:
            human (covid19sim.simulator.Human): human to add
        """
        human.obs_hospitalized = True
        human.obs_in_icu = True
        super().add_human(human)

    def remove_human(self, human):
        """
        Remove a human from the ICU's Ordered set.
        On top of Location.remove_human(), the human's obs_hospitalized and
        obs_in_icu attributes are set to False

        Args:
            human (covid19sim.simulator.Human): human to remove
        """
        human.obs_hospitalized = False
        human.obs_in_icu = False
        super().remove_human(human)

class Event:
    """
    [summary]
    """
    test = 'test'
    encounter = 'encounter'
    contamination = 'contamination'
    recovered = 'recovered'
    static_info = 'static_info'
    visit = 'visit'
    daily = 'daily'

    @staticmethod
    def members():
        """
        DEPRECATED
        """
        return [Event.test, Event.encounter, Event.contamination, Event.static_info, Event.visit, Event.daily]

    @staticmethod
    def log_encounter(human1, human2, location, duration, distance, infectee, time):
        """
        Logs the encounter between `human1` and `human2` at `location` for `duration`
        while staying at `distance` from each other. If infectee is not None, it is
        either human1.name or human2.name.

        Each of the two humans gets its `events` attribute appended whit a dictionnary
        describing the encounter:

        human.events.append({
                'human_id':human.name,
                'event_type':Event.encounter,
                'time':time,
                'payload':{
                    'observed': obs_payload,  # None if one of the humans does not have
                                              # the app. Otherwise contains the observed
                                              # data: lat, lon, location_type
                    'unobserved':unobs_payload  # unobserved data, see loc_unobs_keys and
                                                # h_unobs_keys
                }
        })

        Args:
            human1 (covid19sim.simulator.Human): One of the encounter's 2 humans
            human2 (covid19sim.simulator.Human): One of the encounter's 2 humans
            location (covid19sim.base.Location): Where the encounter happened
            duration (int): duration of encounter
            distance (float): distance between people (TODO: meters? cm?)
            infectee (str | None): name of the human which is infected, if any.
                None otherwise
            time (datetime.datetime): timestamp of encounter
        """

        h_obs_keys   = ['obs_hospitalized', 'obs_in_icu',
                        'obs_lat', 'obs_lon']

        h_unobs_keys = ['carefulness', 'viral_load', 'infectiousness',
                        'symptoms', 'is_exposed', 'is_infectious',
                        'infection_timestamp', 'is_really_sick',
                        'is_extremely_sick', 'sex',  'wearing_mask', 'mask_efficacy',
                        'risk', 'risk_level', 'rec_level']

        loc_obs_keys = ['location_type', 'lat', 'lon']
        loc_unobs_keys = ['contamination_probability', 'social_contact_factor']

        obs, unobs = [], []

        same_household = (human1.household.name == human2.household.name) & (location.name == human1.household.name)
        for human in [human1, human2]:
            o = {key:getattr(human, key) for key in h_obs_keys}
            obs.append(o)
            u = {key:getattr(human, key) for key in h_unobs_keys}
            u['human_id'] = human.name
            u['location_is_residence'] = human.household == location
            u['got_exposed'] = infectee == human.name if infectee else False
            u['exposed_other'] = infectee != human.name if infectee else False
            u['same_household'] = same_household
            u['infectiousness_start_time'] = None if not u['got_exposed'] else human.infection_timestamp + datetime.timedelta(days=human.infectiousness_onset_days)
            unobs.append(u)

        loc_obs = {key:getattr(location, key) for key in loc_obs_keys}
        loc_unobs = {key:getattr(location, key) for key in loc_unobs_keys}
        loc_unobs['location_p_infection'] = location.contamination_probability / location.social_contact_factor
        other_obs = {'duration':duration, 'distance':distance}
        both_have_app = human1.has_app and human2.has_app
        for i, human in [(0, human1), (1, human2)]:
            if both_have_app:
                obs_payload = {**loc_obs, **other_obs, 'human1':obs[i], 'human2':obs[1-i]}
                unobs_payload = {**loc_unobs, 'human1':unobs[i], 'human2':unobs[1-i]}
            else:
                obs_payload = {}
                unobs_payload = { **loc_obs, **loc_unobs, **other_obs, 'human1':{**obs[i], **unobs[i]},
                                    'human2': {**obs[1-i], **unobs[1-i]} }

            human.events.append({
                'human_id':human.name,
                'event_type':Event.encounter,
                'time':time,
                'payload':{'observed':obs_payload, 'unobserved':unobs_payload}
            })

    @staticmethod
    def log_test(human, time):
        """
        Adds an event to a human's `events` list if COLLECT_LOGS is True.
        Events contains the test resuts time, reported_test_result,
        reported_test_type, test_result_validated, test_type, test_result
        split across observed and unobserved data.

        Args:
            human (covid19sim.simulator.Human): Human whose test should be logged
            time (datetime.datetime): Event's time
        """
        human.events.append(
            {
                'human_id': human.name,
                'event_type': Event.test,
                'time': time,
                'payload': {
                    'observed':{
                        'result': human.reported_test_result,
                        'test_type':human.reported_test_type,
                        'validated_test_result':human.test_result_validated
                    },
                    'unobserved':{
                        'test_type':human.test_type,
                        'result': human.test_result
                    }

                }
            }
        )

    @staticmethod
    def log_daily(human, time):
        """
        Adds an event to a human's `events` list containing daily health information
        like symptoms, infectiousness and viral_load.

        Args:
            human (covid19sim.simulator.Human): Human who's health should be logged
            time (datetime.datetime): Event time
        """
        human.events.append(
            {
                'human_id': human.name,
                'event_type': Event.daily,
                'time': time,
                'payload': {
                    'observed':{
                        "reported_symptoms": human.obs_symptoms
                    },
                    'unobserved':{
                        'infectiousness': human.infectiousness,
                        "viral_load": human.viral_load,
                        "all_symptoms": human.all_symptoms,
                        "covid_symptoms":human.covid_symptoms,
                        "flu_symptoms":human.flu_symptoms,
                        "cold_symptoms":human.cold_symptoms
                    }
                }
            }
        )

    @staticmethod
    def log_exposed(human, source, time):
        """
        [summary]

        Args:
            human ([type]): [description]
            source ([type]): [description]
            time ([type]): [description]
        """
        human.events.append(
            {
                'human_id': human.name,
                'event_type': Event.contamination,
                'time': time,
                'payload': {
                    'observed':{
                    },
                    'unobserved':{
                      'exposed': True,
                      'source':source.name,
                      'source_is_location': 'human' not in source.name,
                      'source_is_human': 'human' in source.name,
                      'infectiousness_start_time': human.infection_timestamp + datetime.timedelta(days=human.infectiousness_onset_days)
                    }
                }
            }
        )

    @staticmethod
    def log_recovery(human, time, death):
        """
        [summary]

        Args:
            human ([type]): [description]
            time ([type]): [description]
            death ([type]): [description]
        """
        human.events.append(
            {
                'human_id': human.name,
                'event_type': Event.recovered,
                'time': time,
                'payload': {
                    'observed':{
                    },
                    'unobserved':{
                        'recovered': not death,
                        'death': death
                    }
                }
            }
        )

    @staticmethod
    def log_static_info(city, human, time):
        """
        [summary]

        Args:
            city ([type]): [description]
            human ([type]): [description]
            time ([type]): [description]
        """
        h_obs_keys = ['obs_preexisting_conditions',  "obs_age", "obs_sex", "obs_is_healthcare_worker"]
        h_unobs_keys = ['preexisting_conditions', "age", "sex", "is_healthcare_worker"]
        obs_payload = {key:getattr(human, key) for key in h_obs_keys}
        unobs_payload = {key:getattr(human, key) for key in h_unobs_keys}

        if human.workplace.location_type in ['healthcare', 'store', 'misc', 'senior_residency']:
            obs_payload['n_people_workplace'] = 'many people'
        elif "workplace" == human.workplace.location_type:
            obs_payload['n_people_workplace'] = 'few people'
        else:
            obs_payload['n_people_workplace'] = 'no people outside my household'

        obs_payload['household_size'] = len(human.household.residents)

        human.events.append(
            {
                'human_id': human.name,
                'event_type':Event.static_info,
                'time':time,
                'payload':{
                    'observed': obs_payload,
                    'unobserved':unobs_payload
                }

            }
        )

class Contacts(object):
    """
    `Human` has a contact book that stores a count of all the past encounters.
    This class is also used to store `messages` that are a privacy preserved way of communicating
    between two `Human`.
    It is used in tracing back to the previous encounters that a `Human` had with other `Human`.
    """
    def __init__(self, has_app, conf):
        """
        Initiliazes a book.
        A book is a dict with keys as humans and values as list of past encounters.
        Encounters are stored for the past TRACING_N_DAYS_HISTORY.

        Args:
            has_app (bool): True if owner (type: Human) has an app, else False
            conf (dict): yaml experiment configuration
        """
        self.conf = conf
        self.messages = []
        self.sent_messages_by_day = defaultdict(list)
        self.messages_by_day = defaultdict(list)
        self.update_messages = []
        # human --> [[date, counts], ...]
        self.book = {}
        self.has_app = has_app

    def add(self, **kwargs):
        """
        Called at the time of an encounter.
        Initializes an entry if the encountee is not in the book.
        Adds 1 to the last entry if the encountee is already in the book.
        """
        human = kwargs.get("human")
        timestamp = kwargs.get("timestamp")

        if human not in self.book:
            self.book[human] = [[timestamp.date(), 1]]
            return

        if timestamp.date() != self.book[human][-1][0]:
            self.book[human].append([timestamp.date(), 1])
        else:
            self.book[human][-1][1] += 1
        self.update_book(human, timestamp.date())

    def update_book(self, human, date=None):
        """
        Deletes the entries older than TRACING_N_DAYS_HISTORY.
        If there are no more contacts for human, delete the id of that human from the book.

        Args:
            human (Human): one of the keys in self.book
            date (datetime.datetime.date, optional): . Defaults to None
        """
        # keep the history of risk levels (transformers)
        if date is None:
            date = self.book[human][-1][0] # last contact date

        remove_idx = -1
        for history in self.book[human]:
            if (date - history[0]).days > self.conf.get('TRACING_N_DAYS_HISTORY'):
                remove_idx += 1
            else:
                break
        self.book[human] = self.book[human][remove_idx:]

        # remove that human from the book
        if len(self.book[human]) == 0:
            self.book.pop(human)

    def send_message(self, owner, tracing_method, order=1, reason="test", payload=None):
        """
        Sends messages to all `Human`s in self.book.

        Args:
            owner (Human): owner of this contact book.
            tracing_method (Tracing): Method of tracing that is being used.
            order (int, optional): Number of hops from the source of the message. Defaults to 1.
            reason (str, optional): Reason for the trigger of this message. Defaults to "test". Possible values - "test", "symptoms", "risk_updates"
            payload (dict, optional): Extra information related to the message. Defaults to None.
        """
        p_contact = tracing_method.p_contact
        delay = tracing_method.delay
        app = tracing_method.app
        if app and not owner.has_app:
            return

        for idx, human in enumerate(self.book):

            redundant_tracing = human.message_info['traced'] and tracing_method.dont_trace_traced
            if redundant_tracing: # manual and digital - no effect of new messages
                continue

            if not app or (app and human.has_app):
                if human.rng.random() < p_contact:
                    self.update_book(human)
                    t = 0
                    if delay:
                        t = _draw_random_discreet_gaussian(
                            self.conf.get("MANUAL_TRACING_DELAY_AVG"),
                            self.conf.get("MANUAL_TRACING_DELAY_STD"),
                            human.rng)

                    total_contacts = sum(map(lambda x:x[1], self.book[human]))
                    human.update_risk(update_messages={'n':total_contacts, 'delay': t, 'order':order, 'reason':reason, 'payload':payload})
                    owner.city.tracker.track_update_messages(owner, human, {'reason':reason})

class EmptyCity(City):
    """
    An empty City environment (no humans or locations) that the user can build with
    externally defined code.  Useful for controlled scenarios and functional testing
    """

    def __init__(self, env, rng, x_range, y_range, conf):
        """

        Args:
            env (simpy.Environment): [description]
            rng (np.random.RandomState): Random number generator
            x_range (tuple): (min_x, max_x)
            y_range (tuple): (min_y, max_y)
            conf (dict): yaml experiment configuration
        """
        self.conf = conf
        self.env = env
        self.rng = rng
        self.x_range = x_range
        self.y_range = y_range
        self.total_area = (x_range[1] - x_range[0]) * (y_range[1] - y_range[0])
        self.n_people = 0
        self.last_date_to_check_tests = self.env.timestamp.date()
        self.test_count_today = defaultdict(int)

        # Get the test type with the lowest preference?
        # TODO - EM: Should this rather sort on 'preference' in descending order?
        self.test_type_preference = list(
            zip(
                *sorted(
                    self.conf.get("TEST_TYPES").items(),
                    key=lambda x:x[1]['preference']
                )
            )
        )[0]

        self.humans = []
        self.households = OrderedSet()
        self.stores = []
        self.senior_residencys = []
        self.hospitals = []
        self.miscs = []
        self.parks = []
        self.schools = []
        self.workplaces = []

    @property
    def start_time(self):
        return datetime.datetime.fromtimestamp(self.env.ts_initial)

    def initWorld(self):
        """
        After adding humans and locations to the city, execute this function to finalize the City
        object in preparation for simulation.
        """
        if self.conf.get('COLLECT_LOGS'):
            self.log_static_info()

        print("Computing preferences")
        self._compute_preferences()
        self.tracker = Tracker(self.env, self)
        # self.tracker.track_initialized_covid_params(self.humans)
        self.intervention = None


import os
import sqlite3

class musicDeduperDBManager(object):
    def __init__(self):
        self.db_path = os.path.dirname(__file__)
        self.sqlite_file = os.path.join(self.db_path, 'mdd.sqlite')

    def initialize_new_db(self, sqlite_full_path):
        """Helper function to initialize the db.
        Caller should make sure it doesn't already exist."""
        if os.path.exists(sqlite_full_path):
            raise Exception('The database was already initialized.')
        conn = sqlite3.connect(sqlite_full_path)
        c = conn.cursor()
        result = c.execute('create table song_library(_id integer primary key autoincrement,\n '\
            +'keep integer default 1,\n'
            +'full_file_path text);')
        c.close()

    def initialize(self, full_db_file_path=None):
        """This is the function that will be called when
        the application is run for the first time that
        will check and initialize everything it needs to
        to run the application db."""
        if full_db_file_path is None:
            full_db_file_path = self.sqlite_file
        if not os.path.exists(full_db_file_path):
            print('Initializing {}'.format(full_db_file_path))
            self.initialize_new_db(full_db_file_path)
        else:
            print('{} exists.'.format(full_db_file_path))


'''
Description: Exercise 7 (Tkinter GUI)
Version: 1.0.0.20210118
Author: Arvin Zhao
Date: 2021-01-18 21:04:02
Last Editors: Arvin Zhao
LastEditTime: 2021-01-18 21:27:10
'''

from tkinter import Tk, Entry, Label, DoubleVar, Button
from tkinter.constants import END

def add() -> None:
    '''
    Add a number to the total.
    '''

    try:
        num = float(entry_num.get())
    except ValueError:
        num = 0

    entry_num.delete(0, END)
    total = total_text.get() + num
    total_text.set(total)

def reset() -> None:
    '''
    Reset the program.
    '''

    entry_num.delete(0, END)
    total_text.set(0)

if __name__ == '__main__':
    window = Tk()
    window.title('Sum')
    window.geometry('300x300')

    entry_num = Entry()
    entry_num.pack()
    entry_num.focus()

    total_text = DoubleVar()
    total_text.set(0)
    label_total = Label(textvariable = total_text)
    label_total.pack()

    button_add = Button(text = 'Add', command = add)
    button_add.pack()

    button_reset = Button(text = 'Reset', command = reset)
    button_reset.pack()

    window.mainloop()


"""
These tests aim to verify that concurrency control is working properly.
"""
import asyncio

import pytest

from poke_env.player import RandomPlayer, SimpleHeuristicsPlayer, cross_evaluate

class MoveCallTrackingRandomPlayer(RandomPlayer):
    def choose_move(self, battle):
        try:
            self.move_history.append(battle.battle_tag)
        except AttributeError:
            self.move_history = [battle.battle_tag]
        return super(MoveCallTrackingRandomPlayer, self).choose_move(battle)

async def simple_cross_evaluation(n_battles, n_concurrent_battles):
    players = [
        SimpleHeuristicsPlayer(
            max_concurrent_battles=n_concurrent_battles, log_level=25
        ),
        MoveCallTrackingRandomPlayer(
            max_concurrent_battles=n_concurrent_battles, log_level=25
        ),
    ]
    await cross_evaluate(players, n_challenges=n_battles)

    for player in players:
        await player.ps_client.stop_listening()

    return players[-1].move_history

async def check_max_concurrent_battle_works(n_battles, n_concurrent_battles):
    result = await asyncio.wait_for(
        simple_cross_evaluation(n_battles, n_concurrent_battles), timeout=n_battles + 5
    )

    times_per_battle = {}
    for i, val in enumerate(result):
        if val not in times_per_battle:
            times_per_battle[val] = []
        times_per_battle[val].append(i)

    start_end = [(values[0], values[-1]) for values in times_per_battle.values()]
    max_concurrent_battles = 0

    for i, val in enumerate(result):
        current_battles = 0
        for start, end in start_end:
            if start <= i <= end:
                current_battles += 1
        max_concurrent_battles = max(max_concurrent_battles, current_battles)

    assert max_concurrent_battles == n_concurrent_battles

@pytest.mark.asyncio
async def test_max_concurrent_battle_works_no_concurrency():
    await check_max_concurrent_battle_works(7, 1)

@pytest.mark.asyncio
async def test_max_concurrent_battle_works_3_concurrent():
    await check_max_concurrent_battle_works(20, 3)

@pytest.mark.asyncio
async def test_max_concurrent_battle_works_5_concurrent():
    await check_max_concurrent_battle_works(30, 5)


from django.db import models

from multiselectfield import MultiSelectField

from dnd5e.models.choices import ALIGNMENT_CHOICES, CONDITIONS, DAMAGE_TYPES, SIZE_CHOICES

from .common import Language, RuleBook, Sense

# TODO Make this choice?
class MonsterType(models.Model):
    name = models.CharField(max_length=64, db_index=True, unique=True)
    orig_name = models.CharField(max_length=64, db_index=True, unique=True)

    class Meta:
        ordering = ['name']
        default_permissions = ()
        verbose_name = 'Тип монстра'
        verbose_name_plural = 'Типы монстров'

    def __repr__(self):
        return f'[{self.__class__.__name__}]: {self.id}'

    def __str__(self):
        return f'{self.name} ({self.orig_name})'

class Monster(models.Model):
    CHALENGE_CHOICES = (
        (10, '0'),
        (25, '1/8'),
        (50, '1/4'),
        (100, '1/2'),
        (200, '1'),
        (450, '2'),
        (700, '3'),
        (1100, '4'),
        (1800, '5'),
        (2300, '6'),
        (2900, '7'),
        (3900, '8'),
        (5000, '9'),
        (5900, '10'),
    )

    name = models.CharField(max_length=64, db_index=True, unique=True, verbose_name='Название')
    orig_name = models.CharField(max_length=64, db_index=True, unique=True, verbose_name='Ориг. название')
    slug = models.SlugField(unique=True, max_length=64, db_index=True, editable=False)
    source = models.ForeignKey(
        RuleBook, on_delete=models.CASCADE, verbose_name='Источник',
        related_name='monsters', related_query_name='monster'
    )
    adventure_only = models.ForeignKey(
        'Adventure', on_delete=models.CASCADE, related_name='+', null=True, blank=True, default=None,
        verbose_name='Для приключения'
    )
    size = models.CharField(max_length=1, choices=SIZE_CHOICES, verbose_name='Размер')
    mtype = models.ForeignKey(
        MonsterType, on_delete=models.CASCADE, related_name='monsters', related_query_name='monster',
        verbose_name='Тип монстра'
    )
    subtype = models.CharField(max_length=64, null=True, default=None, verbose_name='Подтип', blank=True)
    alignment = models.PositiveSmallIntegerField(
        null=True, choices=ALIGNMENT_CHOICES, default=10, verbose_name='Мировозрение'
    )
    armor_class = models.PositiveSmallIntegerField(verbose_name='Класс брони')
    hit_points = models.PositiveIntegerField(verbose_name='Очки здоровья')
    hit_dice = models.CharField(null=True, default=None, max_length=8)
    speed = models.PositiveSmallIntegerField(verbose_name='Скорость')
    strength = models.PositiveSmallIntegerField(verbose_name='Сила')
    dexterity = models.PositiveSmallIntegerField(verbose_name='Ловкость')
    constitution = models.PositiveSmallIntegerField(verbose_name='Телосложение')
    intelligence = models.PositiveSmallIntegerField(verbose_name='Интеллект')
    wisdom = models.PositiveSmallIntegerField(verbose_name='Мудрость')
    charisma = models.PositiveSmallIntegerField(verbose_name='Харизма')
    passive_perception = models.PositiveSmallIntegerField(verbose_name='Пассивная внимательность')
    language = models.ManyToManyField(Language, related_name='+', blank=True)
    challenge = models.PositiveIntegerField(verbose_name='Опастность', choices=CHALENGE_CHOICES)
    description = models.TextField(blank=True, verbose_name='Описание')

    damage_immunity = MultiSelectField(
        verbose_name='Иммунитет к урону', blank=True, null=True, default=None, choices=DAMAGE_TYPES, max_length=56
    )
    damage_vuln = MultiSelectField(
        verbose_name='Уязвимость к урону', blank=True, null=True, default=None, choices=DAMAGE_TYPES, max_length=56
    )
    condition_immunity = MultiSelectField(
        verbose_name='Иммунитет к состоянию', blank=True, null=True, default=None, choices=CONDITIONS, max_length=56
    )

    class Meta:
        default_permissions = ()
        ordering = ['name']
        verbose_name = 'Монстр'
        verbose_name_plural = 'Монстры'

    def __str__(self):
        return f'{self.name} ({self.orig_name})'

    def __repr__(self):
        return f'[{self.__class__.__name__}]: {self.id}'

class MonsterTrait(models.Model):
    monster = models.ForeignKey(
        Monster, on_delete=models.CASCADE, related_name='traits', related_query_name='trait'
    )
    name = models.CharField(max_length=64, db_index=True)
    description = models.TextField()
    order = models.PositiveSmallIntegerField(default=1)

    class Meta:
        ordering = ['monster', 'order']
        default_permissions = ()
        verbose_name = 'Способность монстра'
        verbose_name_plural = 'Способности монстров'
        unique_together = (
            ('monster', 'name'),
            ('monster', 'order'),
        )

    def __repr__(self):
        return f'[{self.__class__.__name__}]: {self.id}'

    def __str__(self):
        return self.name

class MonsterAction(models.Model):
    monster = models.ForeignKey(
        Monster, on_delete=models.CASCADE, related_name='actions', related_query_name='action'
    )
    name = models.CharField(max_length=64, db_index=True)
    order = models.PositiveSmallIntegerField(default=1)
    description = models.TextField()

    class Meta:
        default_permissions = ()
        unique_together = ('monster', 'name')
        ordering = ['monster', 'order']

    def __repr__(self):
        return f'[{self.__class__.__name__}]: {self.id}'

    def __str__(self):
        return self.name

class MonsterSense(models.Model):
    sense = models.ForeignKey(Sense, on_delete=models.CASCADE, related_name='+')
    monster = models.ForeignKey(Monster, on_delete=models.CASCADE, related_name='senses')
    value = models.PositiveSmallIntegerField(null=True, default=None)

    class Meta:
        default_permissions = ()
        unique_together = ('sense', 'monster')

    def __repr__(self):
        return f'[{self.__class__.__name__}]: {self.id}'

    def __str__(self):
        return f'{self.sense.name} {self.value:+}'

class MonsterSkill(models.Model):
    skill = models.ForeignKey('Skill', models.CASCADE, related_name='+')
    monster = models.ForeignKey(Monster, on_delete=models.CASCADE, related_name='skills')
    value = models.SmallIntegerField()

    class Meta:
        default_permissions = ()
        unique_together = ('skill', 'monster')

    def __repr__(self):
        return f'[{self.__class__.__name__}]: {self.id}'

    def __str__(self):
        return f'{self.skill.name} {self.value:+}'


''' Experiment measure performance of multiprocessing.Queue vs Boost C++ shared memory queue'''
import torch
from torch.multiprocessing import Process, Queue, Barrier
from dataclasses import dataclass
from typing import Tuple, Dict
import time
import faster_fifo

import io
from multiprocessing.reduction import ForkingPickler
import pickle
class ConnectionWrapper:
    """Proxy class for _multiprocessing.Connection which uses ForkingPickler to
    serialize objects"""

    def __init__(self, conn):
        self.conn = conn

    def send(self, obj):
        buf = io.BytesIO()
        ForkingPickler(buf, pickle.HIGHEST_PROTOCOL).dump(obj)
        self.send_bytes(buf.getvalue())

    def recv(self):
        buf = self.recv_bytes()
        return pickle.loads(buf)

    def __getattr__(self, name):
        if 'conn' in self.__dict__:
            return getattr(self.conn, name)
        raise AttributeError("'{}' object has no attribute '{}'".format(
            type(self).__name__, 'conn'))

class TorchQueue:

    def __init__(self):
        self.q = faster_fifo.Queue(1105 * 5)

    def put(self, obj):
        buf = io.BytesIO()
        ForkingPickler(buf, pickle.HIGHEST_PROTOCOL).dump(obj)
        v = buf.getvalue()
        self.q.put(v, block=True, timeout=10)

    def get(self):
        buf = self.q.get(block=True, timeout=10)
        return pickle.loads(buf)

@dataclass
class IPCMessage:
    id: int
    data: torch.Tensor
    mfg_tuple: Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]
    timing_info: Dict[str, float]

N = 10000

class Producer(Process):
    def __init__(self, queue: Queue, barrier: Barrier):
        super().__init__()
        self.queue = queue
        self.barrier = barrier

    def run(self):
        t = torch.randn(1000, 1000, device=torch.device('cuda', 0))
        t1 = torch.randn(1000, 1000, device=torch.device('cuda', 0))
        t2 = torch.randn(1000, 1000, device=torch.device('cuda', 0))
        t3 = torch.randn(1000, 1000, device=torch.device('cuda', 0))
        # t = 0
        # t1 = 1
        # t2 = 2
        # t3 = 3
        for i in range(50):
            self.queue.put(IPCMessage(i, t, (t1, t2, t3), {'start': time.perf_counter()}))

        time.sleep(5)

        for i in range(N):
            self.queue.put(IPCMessage(i, t, (t1, t2, t3), {'start': time.perf_counter()}))

        self.barrier.wait()

class Consumer(Process):
    def __init__(self, queue: Queue, barrier: Barrier):
        super().__init__()
        self.queue = queue
        self.barrier = barrier

    def run(self):
        sum = 0
        for i in range(50):
            msg = self.queue.get()

        for i in range(N):
            msg = self.queue.get()
            sum += time.perf_counter() - msg.timing_info['start']

        self.barrier.wait()
        print(sum / N, 's')

def main():
    torch.multiprocessing.set_start_method('spawn')
    torch.cuda.init()
    # Create cuda tensor on device 0 and pass to queue
    # q = TorchQueue()
    # q = faster_fifo.Queue()
    q = torch.multiprocessing.Queue(1000)
    b = Barrier(2)
    p = Producer(q, b)
    c = Consumer(q, b)

    p.start()
    c.start()
    p.join()
    c.join()

if __name__ == '__main__':
    main()


#!/usr/bin/env python
import cv2
import numpy
from commons import minmaxnormalization

def frequency_tuned_saliency(image):
    """
    Frequency Tuned Saliency.
    Find the Euclidean distance between
    the Lab pixel vector in a Gaussian filtered image
    with the average Lab vector for the input image.
    R. Achanta, S. Hemami, F. Estrada and S. Susstrunk,
    Frequency-tuned Salient Region
    Detection, IEEE International Conference on Computer
     Vision and Pattern Recognition

    Args:
        image (numpy.array): an image.

    Returns:
       a 2d image saliency map.
    """
    image = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
    #mean of each channel
    means = []
    for c in range(image.shape[2]):
        means.append(image[:, :, c].mean())
    means = numpy.asarray(means)

    image = cv2.medianBlur(image, 9)
    dist = (image - means) ** 2
    print("mean color is %s" % means)
    salmap = numpy.zeros((dist.shape[0], dist.shape[1]))
    for i in range(dist.shape[0]):
        for j in range(dist.shape[1]):
            salmap[i][j] = numpy.sqrt(dist[i][j].sum())
    return minmaxnormalization(salmap)

if __name__ == '__main__':
    import sys
    import re
    import os
    img = cv2.imread(sys.argv[1])
    basename = os.path.basename(sys.argv[1])
    m = re.search('(.*)\..+', basename)
    stem = m.groups()[0]
    sal = frequency_tuned_saliency(img) * 255
    cv2.imwrite("%s.png" % stem, sal)


from src.boards.board_factory import BoardFactory
from src.clocks.clock_factory import ClockFactory
from src.evaluators.evaluator_factory import EvaluatorFactory
from src.expanders.expander_factory import ExpanderFactory
from src.games.game_factory import GameFactory
from src.players.player_factory import PlayerFactory
from src.utilities import utility

utility.setup_logger()

def test(game_no):
    board = BoardFactory.get_instance().get_board(board_id=BoardFactory.CONNECT4_BOARD)

    ev1 = EvaluatorFactory.get_instance().get_evaluator(evaluator_id=EvaluatorFactory.BOARD_STATE_EVALUATOR)
    ev2 = EvaluatorFactory.get_instance().get_evaluator(evaluator_id=EvaluatorFactory.RANDOM_SIMULATION_EVALUATOR)
    ev3 = EvaluatorFactory.get_instance().get_evaluator(evaluator_id=EvaluatorFactory.RANDOM_EVALUATOR)

    ex1 = ExpanderFactory.get_instance().get_expander(expander_id=ExpanderFactory.RANDOM_EXPANDER, evaluator=ev1)
    ex2 = ExpanderFactory.get_instance().get_expander(expander_id=ExpanderFactory.RANDOM_EXPANDER, evaluator=ev3)

    tp1 = PlayerFactory.get_instance().get_player(ex1 if game_no % 2 == 0 else ex2,
                                                  player_id=PlayerFactory.GENERAL_PLAYER, name="gp1")
    # tp1 = PlayerFactory.get_instance().get_player(player_id=PlayerFactory.HUMAN_PLAYER, name="hp1")
    tp2 = PlayerFactory.get_instance().get_player(ex1 if game_no % 2 == 1 else ex2,
                                                  player_id=PlayerFactory.GENERAL_PLAYER, name="gp2")
    # tp2 = PlayerFactory.get_instance().get_player(player_id=PlayerFactory.HUMAN_PLAYER, name="hp2")

    c1 = ClockFactory.get_instance().get_clock(clock_id=ClockFactory.TIME_PER_MOVE_CLOCK, time_per_move=60,
                                               stop_time_delta=1e-8)
    c2 = ClockFactory.get_instance().get_clock(clock_id=ClockFactory.TIME_PER_MOVE_CLOCK, time_per_move=60,
                                               stop_time_delta=1e-8)

    tg = GameFactory.get_instance().get_game(board, [tp1, tp2], [tp1, tp2], {tp1: c1, tp2: c2},
                                             game_id=GameFactory.TIMED_GAME)
    result = tg.play(verbose=True)
    print(result)

if __name__ == '__main__':
    for i in range(1, 2):
        print('GAME:', i)
        test(i)


import os
import os.path
import urllib.request
import subprocess
from zipfile import ZipFile
import sys
import shutil
from email.utils import formatdate
import argparse

if sys.platform == "win32":
    program_files = os.environ.get("ProgramW6432")
    if program_files is None:
        program_files = os.environ["ProgramFiles"]
    DIFF_PROGRAM = os.path.join(program_files, "git", "usr", "bin", "diff.exe")
else:
    DIFF_PROGRAM = "diff"

jd_wrapper_jd_cli_zip_url = """https://github.com/jd-wrapper/jd-cli/archive/refs/tags/v1.0.0.zip"""

script_path = os.path.dirname(os.path.abspath(__file__))

def desuffix(s, suffix):
    assert s.endswith(suffix)
    return s[:-len(suffix)]

def web_download_file(url, output_filename):
    urllib.request.urlretrieve(url, output_filename)

def contents(filename):
    with open(filename, "rb") as handle:
        return handle.read()

def write_contents(filename, data):
    with open(filename, "wb") as handle:
        handle.write(data)

def diff_u(first_file, second_file):
    p = subprocess.Popen([DIFF_PROGRAM, "-uw", first_file, second_file])
    # -u produces a diff in unified format
    # -w ignores all whitespace differences

    p.wait()
    return p.returncode

def file_pattern_replace(filename, old, new, new_filename=None):
    if new_filename is None:
        new_filename = filename
    data = contents(filename)
    if old in data:
        write_contents(new_filename, data.replace(old, new))

def gradle_build_dir(code_dir):
    if sys.platform == "win32":
        command_file = "gradlew.bat"
    else:
        command_file = "gradlew"

    subprocess.check_call([os.path.join(code_dir, command_file), "assemble"], cwd=code_dir)

def get_downloaded_file(url, dir):
    filename_proper = url.rsplit("/", 1)[-1]
    filename = os.path.join(dir, filename_proper)
    if not os.path.exists(filename):
        print("Downloading %s to %s" % (url, dir))
        web_download_file(url, filename)
    else:
        print("Using already downloaded %d" % filename_proper)
    return filename

def unzip_file_to_dir(zip_file, dest_dir):
    with ZipFile(zip_file, "r") as zipObj:
        zipObj.extractall(path=dest_dir)

class JavaDecompiler(object):
    def __init__(self):
        self.cli_jar = None

    def setup(self):
        jd_cli_dir = os.path.join(script_path, "jd-cli")
        if not os.path.isdir(jd_cli_dir):
            jd_cli_zip_filename = get_downloaded_file(jd_wrapper_jd_cli_zip_url, script_path)

            unzip_file_to_dir(jd_cli_zip_filename, jd_cli_dir)

        code_dir = os.path.join(jd_cli_dir, "jd-cli-1.0.0")

        gradle_build_dir(code_dir)

        libs_dir = os.path.join(code_dir, "build", "libs")
        libs_files = os.listdir(libs_dir)

        libs_files = [x for x in libs_files if "-min" not in x]

        assert len(libs_files) == 1

        jd_cli_jar = os.path.join(libs_dir, libs_files[0])
        assert jd_cli_jar.endswith(".jar")

        print("using jd core %s" % jd_cli_jar)
        self.cli_jar = jd_cli_jar

    def ensure_file_dir_exists(self, dest_file):
        dest_inner_dir = os.path.dirname(dest_file)
        os.makedirs(dest_inner_dir, exist_ok=True)

    def decompile_jar_to_dir(self, jar_filename, dest_dir, class_pathspec):
        if self.cli_jar is None:
            raise FileNotFoundError("jd core location unknown; run setup() first")

        assert os.path.isfile(jar_filename)

        dest_file_rel = desuffix(class_pathspec, ".class") + ".java"
        dest_file = os.path.join(dest_dir, dest_file_rel)
        self.ensure_file_dir_exists(dest_file)

        # see docs at https://github.com/jd-wrapper/jd-cli
        cli_command_line_options = ["-b", jar_filename, class_pathspec]

        output = subprocess.check_output(["java", "-jar", self.cli_jar] + cli_command_line_options)

        write_contents(dest_file, output)

        return dest_file

class ModifiedJavaProject(object):

    def __init__(self, decompiler, original_jar, decomp_dir, extracted_classpath_dir, additional_src_dir):
        """
        This represents a Java project originally distributed as a JAR that is being used with modified code

        @param original_jar: The unmodified original JAR
        @param decomp_dir: A working directory we will decompile individual original classes to
        @param extracted_classpath_dir: A classpath directory for runs with the modifications, that contains
                                        all original class files from the original JAR that we are using as-is in
                                        the modified version
        @param additional_src_dir: The source directory with the additional source added in the modification,
                                   that we will also add generated source files to
        """

        assert isinstance(decompiler, JavaDecompiler)
        self.decompiler = decompiler
        self.original_jar_filename = original_jar
        self.decomp_dir = decomp_dir
        self.extracted_classpath_dir = extracted_classpath_dir
        self.src_dir = additional_src_dir

    def additional_src_filename(self, class_pathspec):
        return os.path.join(self.src_dir, desuffix(class_pathspec, ".class") + ".java")

    def class_name_proper(self, class_pathspec):
        return desuffix(os.path.basename(class_pathspec), ".class")

    def common_decompile_and_remove_unpatched(self, class_pathspec):
        """
        Decompile the original class from the JAR
        @param class_pathspec: fully qualified class file specifier using forward slash as the separator and ending with ".class"
        @return: the filename of the extracted source file
        """

        class_name = self.class_name_proper(class_pathspec)

        decompiled_code_filename = self.decompiler.decompile_jar_to_dir(self.original_jar_filename, self.decomp_dir, class_pathspec)

        our_version = self.additional_src_filename(class_pathspec)

        # If the class file is still in the extracted version, remove it, since we want to use the one built from the modified source code instead
        extracted_class_file = os.path.join(self.extracted_classpath_dir, class_pathspec)
        if os.path.isfile(extracted_class_file):
            print("deleting " + extracted_class_file)
            os.remove(extracted_class_file)

        return decompiled_code_filename

    def make_constructor_accessible(self, class_pathspec, show_differences):
        """
        Decompile the source for the given class and make the default (class Foo()) constructor accessible
        @param class_pathspec: fully qualified class file specifier using forward slash as the separator and ending with ".class"
        """

        decompiled_code_filename = self.common_decompile_and_remove_unpatched(class_pathspec)

        # Create a version of the extracted file with added constructor

        new_filename = self.additional_src_filename(class_pathspec)

        class_name = self.class_name_proper(class_pathspec)

        orig_class_start_fragment = "public class %s {" % class_name

        orig_class_start_fragment_bytes = orig_class_start_fragment.encode("utf-8")

        orig_contents = contents(decompiled_code_filename)
        assert orig_class_start_fragment_bytes in orig_contents

        private_constructor_start = "private %s() {" % class_name
        has_private_constructor = private_constructor_start.encode("utf-8") in orig_contents
        protected_constructor_start = "protected %s() {" % class_name
        has_protected_constructor = protected_constructor_start.encode("utf-8") in orig_contents
        has_public_constructor = ("public %s() {" % class_name).encode("utf-8") in orig_contents

        if has_protected_constructor or has_public_constructor:
            print("class %s already has an accessible default constructor")
            shutil.copy(decompiled_code_filename, new_filename)
            return

        note = "// protected constructor version generated by %s %s\n" % (sys.argv[0], formatdate())
        print("replacing " + new_filename)

        if has_private_constructor:
            # we need to change the visibility of the existing constructor
            orig_class_start_fragment_bytes = private_constructor_start.encode("utf-8")
            replacement_fragment = note + protected_constructor_start
            replacement_fragment_bytes = replacement_fragment.encode("utf-8")
            self.decompiler.ensure_file_dir_exists(new_filename)
            file_pattern_replace(decompiled_code_filename, orig_class_start_fragment_bytes, replacement_fragment_bytes, new_filename)
        else:
            # we need to add a constructor
            protected_constructor = "protected %s() { }" % class_name

            replacement_fragment = orig_class_start_fragment + "\n" + note + protected_constructor
            replacement_fragment_bytes = replacement_fragment.encode("utf-8")

            self.decompiler.ensure_file_dir_exists(new_filename)
            file_pattern_replace(decompiled_code_filename, orig_class_start_fragment_bytes, replacement_fragment_bytes, new_filename)

        if show_differences:
            diff_u(decompiled_code_filename, new_filename)

    def get_class_fully_qualified_name(self, class_pathspec):
        return desuffix(class_pathspec, ".class").replace("/", ".")

    def replace_constructed_object(self, class_pathspec, original_constructed_pathspec, new_constructed_pathspec, show_differences):
        """ Decompile the given class and replace calls to construct one class of object
        with calls to construct another class of object
        @param class_pathspec: fully qualified class file specifier using forward slash as the separator and ending with ".class"
        @param original_constructed_pathspec: The class of constructor calls to replace
        @param new_constructed_pathspec: The class to replace it with
        """

        decompiled_code_filename = self.common_decompile_and_remove_unpatched(class_pathspec)

        source_code = contents(decompiled_code_filename).decode("utf-8")

        note = "// alternate constructor call version generated by %s %s\n" % (sys.argv[0], formatdate())

        # note goes at top
        source_code = note + source_code

        full_original_constructed_class_name = self.get_class_fully_qualified_name(original_constructed_pathspec)

        constructor_match_classes = [full_original_constructed_class_name]

        has_original_class_import = ("import %s;" % full_original_constructed_class_name) in source_code

        if has_original_class_import:
            constructor_match_classes.append(self.class_name_proper(original_constructed_pathspec))

        full_replacement_constructed_class_name = self.get_class_fully_qualified_name(new_constructed_pathspec)

        some_match = False
        for cons_match in constructor_match_classes:
            constructor_call_form = "new %s("
            constructor_call_match = constructor_call_form % cons_match
            print(constructor_call_match)
            if constructor_call_match in source_code:
                some_match = True
            source_code = source_code.replace(constructor_call_match, constructor_call_form % full_replacement_constructed_class_name)

        assert some_match, "no matching constructor calls found"

        self.common_finish_contents(class_pathspec, source_code, decompiled_code_filename, show_differences)

    def make_methods_accessible(self, class_pathspec, method_specs_list, show_differences):
        """
        Decompile the source for the given class and make the given methods visible
        @param method_specs_list: a list of specifiers for methods to make visible,
                                    where a specifier is a string with a return type like "void foo"
        @param class_pathspec: fully qualified class file specifier using forward slash as the separator and ending with ".class"
        """

        decompiled_code_filename = self.common_decompile_and_remove_unpatched(class_pathspec)

        source_code = contents(decompiled_code_filename).decode("utf-8")

        note = "// accessible methods version generated by %s %s\n" % (sys.argv[0], formatdate())

        # note goes at top
        source_code = note + source_code

        for method_spec in method_specs_list:
            source_code = source_code.replace("private %s(" % method_spec, "protected %s(" % method_spec)

        self.common_finish_contents(class_pathspec, source_code, decompiled_code_filename, show_differences)

    def common_finish_contents(self, class_pathspec, source_code_str, decompiled_code_filename, show_differences):
        new_filename = self.additional_src_filename(class_pathspec)

        print("replacing " + new_filename)
        self.decompiler.ensure_file_dir_exists(new_filename)
        write_contents(new_filename, source_code_str.encode("utf-8"))

        if show_differences:
            diff_u(decompiled_code_filename, new_filename)

def get_java_bin_path(program):
    assert program == "javac" or program == "jar"
    if program == "javac":
        desc = "A Java compiler"
    elif program == "jar":
        desc = "The jar tool"

    if sys.platform == "win32":
        general_instructions = "\nPlease make sure you have a JDK installed and the JAVA_HOME environment variable is set to its path in Advanced System Settings."
        if "JAVA_HOME" not in os.environ:
            die("The JAVA_HOME environment variable is not set." + general_instructions)
        java_home = os.environ["JAVA_HOME"]
        if not os.path.isdir(java_home):
            die(("The JAVA_HOME environment variable is set to '%s', which does not appear to be a directory." % java_home) + general_instructions)
        result = os.path.join(java_home, "bin", "%s.exe" % program)
        if not os.path.isfile(result):
            die((desc + " was not found in the JAVA_HOME '%s'" % java_home) + general_instructions)
    else:
        result = program
    return result

def compile_java_dir(src_dir, bin_dir, classpath_dirs=None):
    """
    @param src_dir: Java source directory for the top level package
    @param bin_dir: Destination directory for compiler output class files, top level of package hierarchy
    @param classpath_dirs: List of directories or JARs to use as the classpath in addition to normal system library path when building
    """
    javac = get_java_bin_path("javac")

    source_files = []
    for dirpath, dirnames, filenames in os.walk(src_dir):
        for filename in filenames:
            if filename.endswith(".java"):
                full_filename = os.path.join(dirpath, filename)
                source_files.append(full_filename)

    print("passing javac %d source files" % len(source_files))
    args = [javac, "-d", bin_dir]
    if classpath_dirs is not None:
        classpath = ";".join(classpath_dirs)
        args += ["-cp", classpath]
    args += source_files
    subprocess.check_call(args)

def create_mod_jar(output_jar_filename, classpath_dirs, main_class):
    jar = get_java_bin_path("jar")

    print("creating file with initial files from %s" % classpath_dirs[0])
    subprocess.check_call([jar, "cfe", output_jar_filename, main_class, "-C", classpath_dirs[0], "."])
    for classpath_dir in classpath_dirs[1:]:
        print("adding %s" % classpath_dir)
        subprocess.check_call([jar, "uf", output_jar_filename, "-C", classpath_dir, "."])

def parse_args():
    parser = argparse.ArgumentParser("get_sources")
    parser.add_argument("mewnbase_directory", help="The directory containing the original mewnbase game")
    parser.add_argument("--show_differences", default=False, action="store_true", help="Show differences of the patched decompiled class source; requires a diff tool")

    return parser.parse_args()

def die(msg):
    print(msg, file=sys.stderr)
    sys.exit(1)

def step(name):
    print("* " + name)

def main():
    options = parse_args()

    decomp_dir = os.path.join(script_path, "decomp")
    if not os.path.isdir(decomp_dir):
        os.mkdir(decomp_dir)

    extracted_classpath_dir = os.path.join(script_path, "extracted_jar")
    if not os.path.isdir(extracted_classpath_dir):
        os.mkdir(extracted_classpath_dir)

    show_differences = options.show_differences

    # FIXME dynamically determine the existing game location to use
    mewnbase_dir = options.mewnbase_directory
    mewnbase_jar = os.path.join(mewnbase_dir, "game", "desktop-1.0.jar")

    if not os.path.isfile(mewnbase_jar):
        die("Main game JAR file %s not found; double check mewnbase_dir is really %s" % (mewnbase_jar, mewnbase_dir))

    step("Extracting game code class files from JAR")
    unzip_file_to_dir(mewnbase_jar, extracted_classpath_dir)

    jd = JavaDecompiler()
    jd.setup()

    step("Decompiling and modifying game files...")
    print(mewnbase_jar)

    additional_src_dir = os.path.abspath(os.path.join(script_path, "..", "src"))

    modifiedGame = ModifiedJavaProject(jd, mewnbase_jar, decomp_dir, extracted_classpath_dir, additional_src_dir)

    # Make the necessary modifications to classes:

    # constructor visibility
    for class_pathspec in ["com/cairn4/moonbase/tiles/TileFactory.class",
                           "com/cairn4/moonbase/ItemFactory.class"]:

        modifiedGame.make_constructor_accessible(class_pathspec, show_differences)

    # replacement classes
    modifiedGame.replace_constructed_object("com/cairn4/moonbase/ui/GameScreen.class", "com/cairn4/moonbase/GameLoader.class", "net/rakslice/mewnbase_mod/AltGameLoader.class",
                                            show_differences)

    # method visibility
    modifiedGame.make_methods_accessible("com/cairn4/moonbase/GameLoader.class", ["void loadEntities"], show_differences)

    # # For getting a diff for our own hand-modified file we would do something like
    # class_pathspec = "com/cairn4/moonbase/entities/Buggie.class"
    #
    # decompiled_code_filename = jd.decompile_jar_to_dir(mewnbase_jar, decomp_dir, class_pathspec)
    # our_version = os.path.join(script_path, "..", "src", desuffix(class_pathspec, "Buggie.class") + "Train.java")
    #
    # diff_u(decompiled_code_filename, our_version)

    step("Compiling added mod source")
    bin_dir = os.path.abspath(os.path.join(script_path, "..", "bin"))
    compile_java_dir(additional_src_dir, bin_dir, classpath_dirs=[extracted_classpath_dir])

    step("Assemble modded game jar")
    output_jar_filename = os.path.join(script_path, "build", "desktop-1.0.jar")
    jd.ensure_file_dir_exists(output_jar_filename)
    if os.path.exists(output_jar_filename):
        os.remove(output_jar_filename)
    main_class = "com.cairn4.moonbase.desktop.ModDesktopLauncher"
    create_mod_jar(output_jar_filename, [bin_dir, extracted_classpath_dir], main_class)

    step("Done. Output at: %s" % output_jar_filename)

if __name__ == "__main__":
    main()


from individual import *
import itertools

class Population:
	def __init__(self):
		self.population = []

	def AddIndividual(self,individual):
		self.population.append(individual)

	def Show(self):
		for i in range(0,len(self.population)):
			for j in range(0,len(self.population[i].Get())):
				print(self.population[i].Get()[j].Get(),end="")
			print()

	def Get(self):
		return self.population

	def Getn(self,n):
		return self.population[n]

	def GetSize(self):
		return len(self.population)

	def Check_if_correct(self):
		pass

	def AddStart(self,start,end):
		for i in self.population:
			for j in i.Get():
				j.Get().update(start)
				j.Get().move_to_end("START", last=False)
				j.Get().update(end)
				i.AddLength(j)

	def RemoveStart(self):
		for i in self.population:
			for j in i.Get():
				if "START" in j.Get():
					j.Get().popitem(last=False)
					j.Get().popitem()
			i.ResetLength()

	def Crossing(self):		#crossing for individuals with the same amount of places in every route
		child = Individual()
		parent1 = randint(0,self.GetSize()-1)
		while True:
			parent2 = randint(0,self.GetSize()-1)
			if parent2 != parent1:
				break

		#for Individual that has at least 3 Routes
		parent1_route = self.population[parent1].Get()[1]	#second Route from parent1
		#print(self.population[parent1].GetSize())
		for i in range(0,self.population[parent1].GetSize()):
			route = Route()
			if i == 1:
				child.AddRoute(self.population[parent1].Get()[1])
			if i != 1:
				for j in range(0,self.population[parent2].GetSize()):
					for place, position in self.population[parent2].Get()[j].Get().items():
						if parent1_route.Check_place(place) == False and child.Check_place(place) == False and route.GetSize() < self.population[parent1].Get()[i].GetSize():
							route.AddPlace(place,position)
						#print(place,position,end="")
				child.AddRoute(route)
				#print()

		return child
		'''print(child.Check_if_correct())
		self.population[parent1].Show()
		print()
		self.population[parent2].Show()
		print()
		child.Show()'''
		#print(self.population[parent1].Get()[0].Get())

	def CrossingMerged(self):	#crossing for merged individuals
		child = Individual()
		parent1 = randint(0,self.GetSize()-1)
		while True:
			parent2 = randint(0,self.GetSize()-1)
			if parent2 != parent1:
				break

		child_routes_length = []

		if self.population[parent1].GetSize() == 1:
			child_routes_length.append(len(self.population[parent1].Merge()))
		else:
			child_routes_length.append(randint(1,len(self.population[parent1].Merge())//2))
		#print(child_routes_length[0])
		def sum(child_routes_length):
			sum = 0
			for i in child_routes_length:
				sum += i
			return sum

		for i in range(0,self.population[parent1].GetSize()-2):	# bo jedna dlugosc dodalismy wyzej i jedna dodamy nizej
			if sum(child_routes_length) + self.population[parent1].Getn(i).GetSize() < len(self.population[parent1].Merge()):
				child_routes_length.append(self.population[parent1].Getn(i).GetSize())
			elif sum(child_routes_length) + self.population[parent2].Getn(i).GetSize() < len(self.population[parent1].Merge()):
				child_routes_length.append(self.population[parent2].Getn(i).GetSize())
			else:
				child_routes_length.append(1)

		if self.population[parent1].GetSize() == 1:
			route = Route()
			wanted_items = list(self.population[parent1].Merge().items())[:child_routes_length[0]//2]
			for place, position in wanted_items:
				route.AddPlace(place,position)
			for place, position in self.population[parent2].Merge().items():
				#print(place,position)
				if child.Check_place(place) == False and route.Check_place(place) == False:
					route.AddPlace(place, position)
			child.AddRoute(route)

		else:
			child_routes_length.append(len(self.population[parent1].Merge())-sum(child_routes_length))

			for i in range(0,len(child_routes_length)):
				route = Route()
				if i == 0:
					wanted_items = list(self.population[parent1].Merge().items())[:child_routes_length[i]]
					for place, position in wanted_items:
						route.AddPlace(place,position)
				if i > 0:
					for j in range(0,child_routes_length[i]):
						#print(child_routes_length[i])
						for place, position in self.population[parent2].Merge().items():
							#print(place,position)
							if child.Check_place(place) == False and route.Check_place(place) == False:
								route.AddPlace(place, position)
								break
				child.AddRoute(route)

		#print(child_routes_length)
		#child.Show()
		return child

	def Mutation(self):	# Mutated individual merged is reversed (permutated in new version) parent with different routes lengths
		parent = randint(0,self.GetSize()-1)
		mutated = Individual()
		#print("parent ", parent)

		def sum(new_routes_length):
			sum = 0
			for i in new_routes_length:
				sum += i
			return sum

		new_routes_length = []

		if self.population[parent].GetSize() == 1:
			new_routes_length.append(len(self.population[parent].Merge()))
		else:
			new_routes_length.append(randint(1,len(self.population[parent].Merge())//2))
			for i in range(0,self.population[parent].GetSize()-2):
				route_length = randint(1,len(self.population[parent].Merge()) - sum(new_routes_length) - 1)
				new_routes_length.append(route_length)

			new_routes_length.append(len(self.population[parent].Merge())-sum(new_routes_length))

		#print(self.population[parent].Merge())
		items = list(self.population[parent].Merge().items())
		shuffle(items)
		self.population[parent].Merge().clear()
		self.population[parent] = OrderedDict(items)
		#print(self.population[parent])

		for i in range(0,len(new_routes_length)):
			route = Route()
			for j in range(0,new_routes_length[i]):
				#print(child_routes_length[i])
				for place, position in self.population[parent].items():
					#print(place,position)
					if mutated.Check_place(place) == False and route.Check_place(place) == False:
						route.AddPlace(place, position)
						break
			mutated.AddRoute(route)

		self.population[parent] = mutated

	def SortPopulation(self):
		def GetIndividualLength(individual):
			return individual.GetLength()
		self.population.sort(key=GetIndividualLength)

	def BestIndividual(self):
		min_length = self.population[0].GetLength()
		best = self.population[0]
		for i in range(1,len(self.population)):
			if self.population[i].GetLength() < min_length:
				min_length = self.population[i].GetLength()
				best = self.population[i]
		return best

	def LeavenBest(self,n):
		for i in range(n,self.GetSize()):
			self.population.pop()


__all__ = [
    'Comment',
    'CommentOnSpectrum',
    'CommentOnGCN',
    'CommentOnShift',
]

import sqlalchemy as sa
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relationship
from sqlalchemy.orm import deferred

from baselayer.app.models import (
    Base,
    AccessibleIfRelatedRowsAreAccessible,
    AccessibleIfUserMatches,
)

from .group import accessible_by_groups_members

"""
NOTE ON ADDING NEW COMMENT TYPES:
To add a new comment on <something> you need to
- Inherit from CommentMixin (as well as Base).
- Add the name of the table to backref_name on CommentMixin
- Add any additional columns, like references to a model the comment is on.
- Add the comment as a relationship with back_populates (etc.) on the model you are commenting on.
  (e.g., for CommentOnSpectrum you need to add "comments" to models/spectrum.py)
- Add a join to models/group_joins.py so comments will have groups associated with them.
- Add a join to models/user_token.py so comments will have a user associated with them.
- Update the API endpoints for comments, and the reducers to listen for changes in the comments.
- Update the app_server.py paths to accept the new type of comment upon API calls.

"""

class CommentMixin:
    text = sa.Column(sa.String, nullable=False, doc="Comment body.")

    attachment_name = sa.Column(
        sa.String, nullable=True, doc="Filename of the attachment."
    )

    @declared_attr
    def attachment_bytes(cls):
        return deferred(
            sa.Column(
                sa.types.LargeBinary,
                nullable=True,
                doc="Binary representation of the attachment.",
            )
        )

    origin = sa.Column(sa.String, nullable=True, doc='Comment origin.')

    bot = sa.Column(
        sa.Boolean(),
        nullable=False,
        server_default="false",
        doc="Boolean indicating whether comment was posted via a bot (token-based request).",
    )

    @classmethod
    def backref_name(cls):
        if cls.__name__ == 'Comment':
            return "comments"
        if cls.__name__ == 'CommentOnSpectrum':
            return 'comments_on_spectra'
        if cls.__name__ == 'CommentOnGCN':
            return 'comments_on_gcns'
        if cls.__name__ == 'CommentOnShift':
            return 'comments_on_shifts'

    @declared_attr
    def author(cls):
        return relationship(
            "User",
            back_populates=cls.backref_name(),
            doc="Comment's author.",
            uselist=False,
            foreign_keys=f"{cls.__name__}.author_id",
        )

    @declared_attr
    def author_id(cls):
        return sa.Column(
            sa.ForeignKey('users.id', ondelete='CASCADE'),
            nullable=False,
            index=True,
            doc="ID of the Comment author's User instance.",
        )

    @declared_attr
    def groups(cls):
        return relationship(
            "Group",
            secondary="group_" + cls.backref_name(),
            cascade="save-update, merge, refresh-expire, expunge",
            passive_deletes=True,
            doc="Groups that can see the comment.",
        )

    def construct_author_info_dict(self):
        return {
            field: getattr(self.author, field)
            for field in ('username', 'first_name', 'last_name', 'gravatar_url')
        }

class Comment(Base, CommentMixin):
    """A comment made by a User or a Robot (via the API) on a Source."""

    create = AccessibleIfRelatedRowsAreAccessible(obj='read')

    read = accessible_by_groups_members & AccessibleIfRelatedRowsAreAccessible(
        obj='read'
    )

    update = delete = AccessibleIfUserMatches('author')

    obj_id = sa.Column(
        sa.ForeignKey('objs.id', ondelete='CASCADE'),
        nullable=False,
        index=True,
        doc="ID of the Comment's Obj.",
    )

    obj = relationship(
        'Obj',
        back_populates='comments',
        doc="The Comment's Obj.",
    )

class CommentOnSpectrum(Base, CommentMixin):

    __tablename__ = 'comments_on_spectra'

    create = AccessibleIfRelatedRowsAreAccessible(obj='read', spectrum='read')

    read = accessible_by_groups_members & AccessibleIfRelatedRowsAreAccessible(
        obj='read',
        spectrum='read',
    )

    update = delete = AccessibleIfUserMatches('author')

    obj_id = sa.Column(
        sa.ForeignKey('objs.id', ondelete='CASCADE'),
        nullable=False,
        index=True,
        doc="ID of the Comment's Obj.",
    )

    obj = relationship(
        'Obj',
        back_populates='comments_on_spectra',
        doc="The Comment's Obj.",
    )

    spectrum_id = sa.Column(
        sa.ForeignKey('spectra.id', ondelete='CASCADE'),
        nullable=False,
        index=True,
        doc="ID of the Comment's Spectrum.",
    )
    spectrum = relationship(
        'Spectrum',
        back_populates='comments',
        doc="The Spectrum referred to by this comment.",
    )

class CommentOnGCN(Base, CommentMixin):

    __tablename__ = 'comments_on_gcns'

    create = AccessibleIfRelatedRowsAreAccessible(gcn='read')

    read = accessible_by_groups_members & AccessibleIfRelatedRowsAreAccessible(
        gcn='read',
    )

    update = delete = AccessibleIfUserMatches('author')

    gcn_id = sa.Column(
        sa.ForeignKey('gcnevents.id', ondelete='CASCADE'),
        nullable=False,
        index=True,
        doc="ID of the Comment's GCN.",
    )
    gcn = relationship(
        'GcnEvent',
        back_populates='comments',
        doc="The GcnEvent referred to by this comment.",
    )

class CommentOnShift(Base, CommentMixin):

    __tablename__ = 'comments_on_shifts'

    create = AccessibleIfRelatedRowsAreAccessible(shift='read')

    read = accessible_by_groups_members & AccessibleIfRelatedRowsAreAccessible(
        shift='read',
    )

    update = delete = AccessibleIfUserMatches('author')

    shift_id = sa.Column(
        sa.ForeignKey('shifts.id', ondelete='CASCADE'),
        nullable=False,
        index=True,
        doc="ID of the Comment's Shift.",
    )
    shift = relationship(
        'Shift',
        back_populates='comments',
        doc="The Shift referred to by this comment.",
    )


FLAVORS = [
    "Banana",
    "Chocolate",
    "Lemon",
    "Pistachio",
    "Raspberry",
    "Strawberry",
    "Vanilla",
]

mylist = list()
for i in FLAVORS:
    for j in FLAVORS:
        if i != j and ([j,i] not in mylist):
                mylist.append([i,j])

# print(mylist)
for i in mylist:
    print(f'{i[0]}, {i[1]}')

Python - Realist/02 Print sorbet flavors.py