class BestMoleculeHere(MoleculeException):

    def __init__(self, best_distance,  *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._best_distance = best_distance

    def get_best_distance(self):
        return self._best_distance POINT_SMELL = IncrementedNamedInt.get('intelligine.point_smell') POINTS_SMELL = IncrementedNamedInt.get('intelligine.points_smell') MOLECULES_INFOS = IncrementedNamedInt.get('intelligine.mol.infos') MOLECULES_DIRECTION = IncrementedNamedInt.get('intelligine.mol.direction') SMELL_FOOD = IncrementedNamedInt.get('intelligine.smell.food') SMELL_EGG = IncrementedNamedInt.get('intelligine.smell.egg') PHEROMON_DIR_EXPLO = IncrementedNamedInt.get('intelligine.phero.direction.explo') class DirectionMolecule():

    WAY_UP = 'u'
    WAY_DOWN = 'd'

    _positions_key = None

    @classmethod
    def appose(cls, context, point, molecule):
        context.molecules().increment_with_molecule(point, molecule, context.get_cycle())
        context.metas.list.add(MOLECULES, MOLECULES, point, assert_not_in=False)

    @classmethod
    def get_direction_for_point(cls, context, point, molecule_type, molecule_way=WAY_UP):
        flavour = context.molecules().get_flavour(point)
        molecule = flavour.get_molecule(category=MOLECULES_DIRECTION, type=molecule_type)

        distance = molecule.get_distance()
        around_molecule_filter = lambda around_molecule: around_molecule.get_distance() <= distance #  TODO <= ?
        if molecule_way == cls.WAY_DOWN:
            around_molecule_filter = lambda around_molecule: around_molecule.get_distance() >= distance
        around_molecules_points = cls._get_around_molecules(context, point, molecule_type,
                                                            molecule_filter=around_molecule_filter)

        if not around_molecules_points \
           or (len(around_molecules_points) == 1 and around_molecules_points[0][0] == point):
            raise NoMolecule()

        shuffle(around_molecules_points)
        around_molecules_sorted = sorted(around_molecules_points, key=lambda x: x[1].get_intensity(), reverse=True)
        max_intensity = around_molecules_sorted[0][1].get_intensity()

        around_molecules_max = []
        for around_molecule_sorted in around_molecules_sorted:
            if around_molecule_sorted[1].get_intensity() == max_intensity:
                around_molecules_max.append(around_molecule_sorted)

        reverse = False
        if molecule_way == cls.WAY_DOWN:
            reverse = True
        around_molecules_sorted_by_distance = sorted(around_molecules_max,
                                                      key=lambda x: x[1].get_distance(),
                                                      reverse=reverse)

        all_same_distance = True
        for mol in around_molecules_sorted_by_distance:
            mol_mol = mol[1]
            if mol_mol.get_distance() != distance:
                all_same_distance = False
                break

        if all_same_distance:
            raise NoMolecule()

        go_to_point = around_molecules_sorted_by_distance[0][0]

        direction_degrees = get_degree_from_north(point, go_to_point)
        direction = get_direction_for_degrees(direction_degrees)

        return direction

    @classmethod
    def _get_around_molecules(cls, context, reference_point, molecule_type,
                              molecule_filter=lambda around_molecule: True):
        around_points = context.get_around_points_of_point(reference_point)
        around_molecules_points = []
        for around_point in around_points:
            flavour = context.molecules().get_flavour(around_point)
            try:
                around_molecule = flavour.get_molecule(category=MOLECULES_DIRECTION, type=molecule_type)
                if molecule_filter(around_molecule):
                    around_molecules_points.append((around_point, around_molecule))
            except NoMolecule:
                pass  # No molecule, ok continue to sniff around

        return around_molecules_points

    @classmethod
    def get_best_molecule_direction_in(cls, context, reference_point, points, molecule_type):
        around_molecules_points = []
        for around_point in points:
            flavour = context.molecules().get_flavour(around_point)
            try:
                around_molecule = flavour.get_molecule(category=MOLECULES_DIRECTION, type=molecule_type)
                around_molecules_points.append((around_point, around_molecule))
            except NoMolecule:
                pass  # Ok, no molecule, continue to sniff around

        if not around_molecules_points \
           or (len(around_molecules_points) == 1 and around_molecules_points[0][0] == reference_point):
            raise NoMolecule()

        shuffle(around_molecules_points)
        around_molecules_sorted = sorted(around_molecules_points, key=lambda x: x[1].get_intensity(), reverse=True)
        go_to_point = around_molecules_sorted[0][0]

        direction_degrees = get_degree_from_north(reference_point, go_to_point)
        direction = get_direction_for_degrees(direction_degrees)

        return direction class Evaporation:

    def __init__(self, context,
                 intensity_decrement=1,
                 molecule_minimum_age=0,
                 molecules_exclude_types=None,
                 molecules_include_types=None):
        self._context = context
        self._intensity_decrement = intensity_decrement
        self._molecules_manager = MoleculesManager(context)
        self._molecule_minimum_age = molecule_minimum_age
        self._molecules_exclude_types = molecules_exclude_types
        self._molecules_include_types = molecules_include_types

    def evaporate(self):
        for position, flavour in self._get_flavours():
            self._decrease_flavour(flavour)
            self._molecules_manager.set_flavour(position, flavour)

    def remove(self):
        for position, flavour in self._get_flavours():
            self._remove_molecule(flavour)
            self._molecules_manager.set_flavour(position, flavour)

    def _get_flavours(self):
        molecules_points = self._context.metas.list.get(MOLECULES, MOLECULES, allow_empty=True)
        for molecule_point in molecules_points:
            yield molecule_point, self._molecules_manager.get_flavour(molecule_point)

    def _decrease_flavour(self, flavour):
        for direction_molecule in self._get_molecules_from_flavour(flavour):
            direction_molecule.increment_intensity(-self._intensity_decrement)
            flavour.set_molecule(direction_molecule)

    def _get_molecules_from_flavour(self, flavour):
        molecules = []
        for direction_molecule in flavour.get_molecules(MOLECULES_DIRECTION):
            if not self._is_recent_molecule(direction_molecule) \
               and not self._is_excluded_molecule_type(direction_molecule):
                molecules.append(direction_molecule)
        return molecules

    def _remove_molecule(self, flavour):
        for direction_molecule in self._get_molecules_from_flavour(flavour):
            flavour.remove_molecule(direction_molecule)

    def _is_recent_molecule(self, molecule):
        return (self._context.get_cycle() - molecule.get_cycle_age()) < self._molecule_minimum_age

    def _is_excluded_molecule_type(self, molecule):
        if not self._molecules_exclude_types and not self._molecules_include_types:
            return False

        if self._molecules_exclude_types and not self._molecules_include_types:
            return molecule.get_type() in self._molecules_exclude_types

        if not self._molecules_exclude_types and self._molecules_include_types:
            return molecule.get_type() not in self._molecules_include_types class Molecule():

    def __init__(self, category, type, distance=None, intensity=0, cycle_age=None):
        self._category = category
        self._type = type
        self._distance = distance
        self._intensity = intensity
        self._cycle_age = cycle_age

    def get_category(self):
        return self._category

    def get_type(self):
        return self._type

    def get_distance(self):
        return self._distance

    def set_distance(self, distance):
        self._distance = distance

    def get_intensity(self):
        return self._intensity

    def get_cycle_age(self):
        return self._cycle_age

    def set_cycle_age(self, cycle_age):
        self._cycle_age = cycle_age

    def increment_intensity(self, increment_value):
        self._intensity += increment_value class MoveAction(Action):

    _listen = MoveEvent

    def __init__(self, object_id, parameters):
        super().__init__(object_id, parameters)
        self._move_to_point = None
        self._move_to_direction = None

    def run(self, obj, context, synergy_manager):
        if self._parameters.get(MoveEvent.PARAM_NEW_MODE, None):
            obj.get_brain().switch_to_mode(self._parameters[MoveEvent.PARAM_NEW_MODE])
            return

        try:
            self._apply_move(obj, context)
        except ActionAborted:
            # TODO: Dans l'obj ces lignes
            blocked_since = context.metas.value.get(BLOCKED_SINCE, self._object_id, allow_empty=True, empty_value=0)
            context.metas.value.set(BLOCKED_SINCE, obj.get_id(), blocked_since+1)

    def _apply_move(self, obj, context):
        # TODO: il ne faut pas choisir une direction 14.
        if MoveEvent.PARAM_DIRECTION not in self._parameters or self._parameters[MoveEvent.PARAM_DIRECTION] == 14:
            raise ActionAborted()

        obj.set_position(self._parameters[MoveEvent.PARAM_POSITION])
        obj.set_previous_direction(self._parameters[MoveEvent.PARAM_DIRECTION])
        obj.get_brain().get_part(BRAIN_PART_MOVE).done() class SmellEvent(Event):

    _mechanism = TraversableDistanceFromMechanism
    _concern = COL_SMELL
    _each_cycle = 100
    _first_cycle_force = True

    def _prepare(self, object_id, context, parameters={}):
        if not parameters['points_distances'] or not self._concerned_object(context, object_id):
            raise NotConcernedEvent()

        return parameters

    def _concerned_object(self, context, object_id):
        # TODO: Un peu hardcodé etant donné que cet event concerne tout les COL_SMELL et pas que les transportable ...
        return not context.metas.value.get(CARRIED_BY, object_id, allow_empty=True, empty_value=False)
from intelligine.core.exceptions import BestMoleculeHere
from intelligine.cst import POINT_SMELL, POINTS_SMELL, MOLECULES_INFOS, MOLECULES_DIRECTION, SMELL_FOOD, SMELL_EGG, \
    PHEROMON_DIR_EXPLO
from intelligine.simulation.molecule.DirectionMolecule import DirectionMolecule
from intelligine.simulation.molecule.Evaporation import Evaporation
from intelligine.simulation.molecule.Molecule import Molecule
from intelligine.synergy.event.move.MoveAction import MoveAction
from intelligine.synergy.event.smell.SmellEvent import SmellEvent
from synergine.synergy.event.Action import Action


class SmellAction(Action):

    _listen = SmellEvent
    _depend = [MoveAction]

    @classmethod
    def cycle_pre_run(cls, context, synergy_manager):

        evaporation = Evaporation(context, molecules_include_types=[SMELL_FOOD, SMELL_EGG])