# coding: utf-8
import collections
import typing

from synergine2.base import BaseObject
from synergine2.config import Config
from synergine2.utils import get_mechanisms_classes


class Subject(BaseObject):
    collections = []
    behaviours_classes = []
    behaviour_selector_class = None  # type: typing.Type[SubjectBehaviourSelector]

    def __init__(
        self,
        config: Config,
        simulation: 'Simulation',
    ):
        # TODO: Bannir les attribut de classe passé en reference ! Et meme virer les attr de classe tout court.
        self.collections = self.collections[:]

        self.config = config
        self.id = id(self)  # We store object id because it's lost between process
        self.simulation = simulation
        self.behaviours = {}
        self.mechanisms = {}
        self.behaviour_selector = None  # type: SubjectBehaviourSelector
        if self.behaviour_selector_class:
            self.behaviour_selector = self.behaviour_selector_class()

        for collection in self.collections:
            self.simulation.collections[collection].append(self)

        self.initialize()

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        return '{}({})'.format(
            type(self).__name__,
            self.id,
        )

    def initialize(self):
        for mechanism_class in get_mechanisms_classes(self):
            self.mechanisms[mechanism_class] = mechanism_class(
                config=self.config,
                simulation=self.simulation,
                subject=self,
            )

        for behaviour_class in self.behaviours_classes:
            self.behaviours[behaviour_class] = behaviour_class(
                config=self.config,
                simulation=self.simulation,
                subject=self,
            )


class Subjects(list):
    """
    TODO: Manage other list methods
    """
    def __init__(self, *args, **kwargs):
        self.simulation = kwargs.pop('simulation')
        self.removes = []
        self.adds = []
        self.track_changes = False
        self.index = {}
        super().__init__(*args, **kwargs)

    def remove(self, value: Subject):
        # Remove from index
        del self.index[value.id]
        # Remove from subjects list
        super().remove(value)
        # Remove from collections
        for collection_name in value.collections:
            self.simulation.collections[collection_name].remove(value)
        # Add to removed listing
        if self.track_changes:
            self.removes.append(value)

    def append(self, p_object):
        # Add to index
        self.index[p_object.id] = p_object
        # Add to subjects list
        super().append(p_object)
        # Add to adds list
        if self.track_changes:
            self.adds.append(p_object)


class Simulation(BaseObject):
    accepted_subject_class = Subjects
    behaviours_classes = []

    def __init__(
        self,
        config: Config,
    ):
        self.config = config
        self.collections = collections.defaultdict(list)
        self._subjects = None
        self.behaviours = {}
        self.mechanisms = {}

        self.initialize()

    @property
    def subjects(self):
        return self._subjects

    @subjects.setter
    def subjects(self, value: 'Subjects'):
        if not isinstance(value, self.accepted_subject_class):
            raise Exception('Simulation.subjects must be {0} type'.format(
                self.accepted_subject_class,
            ))
        self._subjects = value

    def initialize(self):
        for mechanism_class in get_mechanisms_classes(self):
            self.mechanisms[mechanism_class] = mechanism_class(
                config=self.config,
                simulation=self,
            )

        for behaviour_class in self.behaviours_classes:
            self.behaviours[behaviour_class] = behaviour_class(
                config=self.config,
                simulation=self,
            )


class SubjectMechanism(BaseObject):
    def __init__(
            self,
            config: Config,
            simulation: Simulation,
            subject: Subject,
    ):
        self.config = config
        self.simulation = simulation
        self.subject = subject

    def run(self):
        raise NotImplementedError()


class SimulationMechanism(BaseObject):
    """If parallelizable behaviour, call """
    parallelizable = False

    def __init__(
            self,
            config: Config,
            simulation: Simulation,
    ):
        self.config = config
        self.simulation = simulation

    def repr_debug(self) -> str:
        return self.__class__.__name__

    def run(self, process_id: int=None, process_count: int=None):
        raise NotImplementedError()


class Event(BaseObject):
    def __init__(self, *args, **kwargs):
        pass

    def repr_debug(self) -> str:
        return self.__class__.__name__


class SubjectBehaviour(BaseObject):
    frequency = 1
    use = []

    def __init__(
            self,
            config: Config,
            simulation: Simulation,
            subject: Subject,
    ):
        self.config = config
        self.simulation = simulation
        self.subject = subject

    def run(self, data):
        """
        Method called in subprocess.
        If return equivalent to False, behaviour produce nothing.
        If return equivalent to True, action bahaviour method
        will be called with these data
        Note: Returned data will be transfered from sub processes.
              Prefer scalar types.
        """
        raise NotImplementedError()  # TODO Test it and change to strictly False

    def action(self, data) -> [Event]:
        """
        Method called in main process
        Return events will be give to terminals
        """
        raise NotImplementedError()


class SimulationBehaviour(BaseObject):
    frequency = 1
    use = []

    def __init__(
            self,
            config: Config,
            simulation: Simulation,
    ):
        self.config = config
        self.simulation = simulation

    def run(self, data):
        """
        Method called in subprocess if mechanisms are
        parallelizable, in main process if not.
        """
        raise NotImplementedError()

    @classmethod
    def merge_data(cls, new_data, start_data=None):
        """
        Called if behaviour executed in subprocess
        """
        raise NotImplementedError()

    def action(self, data) -> [Event]:
        """
        Method called in main process
        Return events will be give to terminals
        """
        raise NotImplementedError()


class SubjectBehaviourSelector(BaseObject):
    def reduce_behaviours(
        self,
        behaviours: typing.Dict[typing.Type[SubjectBehaviour], object],
    ) -> typing.Dict[typing.Type[SubjectBehaviour], object]:
        raise NotImplementedError()
