from soil.agents import FSM, NetworkAgent, state, default_state, prob
from soil.parameters import *
import logging

from soil.environment import Environment


class DumbViewer(FSM, NetworkAgent):
    """
    A viewer that gets infected via TV (if it has one) and tries to infect
    its neighbors once it's infected.
    """

    has_been_infected: bool = False
    has_tv: bool = False

    @default_state
    @state
    def neutral(self):
        if self.has_tv:
            if self.prob(self.get("prob_tv_spread")):
                return self.infected
        if self.has_been_infected:
            return self.infected

    @state
    def infected(self):
        for neighbor in self.get_neighbors(state_id=self.neutral.id):
            if self.prob(self.get("prob_neighbor_spread")):
                neighbor.infect()

    def infect(self):
        """
        This is not a state. It is a function that other agents can use to try to
        infect this agent. DumbViewer always gets infected, but other agents like
        HerdViewer might not become infected right away
        """
        self.has_been_infected = True


class HerdViewer(DumbViewer):
    """
    A viewer whose probability of infection depends on the state of its neighbors.
    """

    def infect(self):
        """Notice again that this is NOT a state. See DumbViewer.infect for reference"""
        infected = self.count_neighbors(state_id=self.infected.id)
        total = self.count_neighbors()
        prob_infect = self.get("prob_neighbor_spread") * infected / total
        self.debug("prob_infect", prob_infect)
        if self.prob(prob_infect):
            self.has_been_infected = True


class WiseViewer(HerdViewer):
    """
    A viewer that can change its mind.
    """

    @state
    def cured(self):
        prob_cure = self.get("prob_neighbor_cure")
        for neighbor in self.get_neighbors(state_id=self.infected.id):
            if self.prob(prob_cure):
                try:
                    neighbor.cure()
                except AttributeError:
                    self.debug("Viewer {} cannot be cured".format(neighbor.id))

    def cure(self):
        self.has_been_cured = True

    @state
    def infected(self):
        if self.has_been_cured:
            return self.cured
        cured = max(self.count_neighbors(self.cured.id), 1.0)
        infected = max(self.count_neighbors(self.infected.id), 1.0)
        prob_cure = self.get("prob_neighbor_cure") * (cured / infected)
        if self.prob(prob_cure):
            return self.cured


class NewsSpread(Environment):
    ratio_dumb: probability = 1,
    ratio_herd: probability = 0,
    ratio_wise: probability = 0,
    prob_tv_spread: probability = 0.1,
    prob_neighbor_spread: probability = 0.1,
    prob_neighbor_cure: probability = 0.05,

    def init(self):
        self.populate_network([DumbViewer, HerdViewer, WiseViewer], [self.ratio_dumb, self.ratio_herd, self.ratio_wise])


from itertools import permutations
from soil import Simulation


# We want to investigate the effect of different agent distributions on the spread of news.
# To do that, we will run different simulations, with a varying ratio of DumbViewers, HerdViewers, and WiseViewers
# Because the effect of these agents might also depend on the network structure, we will run our simulations on two different networks:
# one with a small-world structure and one with a connected structure.

for [r1, r2, r3] in permutations([0, 0.5, 1.0], 3):
    for (generator, netparams) in {
        "barabasi_albert_graph": {"m": 5},
        "erdos_renyi_graph": {"p": 0.1},
    }.items():
        print(r1, r2, r3, generator)
        # Create new simulation
        netparams["n"] = 500
        sim = Simulation(
            model=NewsSpread,
            model_params={
                "ratio_dumb": r1,
                "ratio_herd": r2,
                "ratio_wise": r3,
                "network_generator": generator,
                "network_params": netparams,
                "prob_neighbor_spread": 0,
            },
            num_trials=50,
            max_steps=300,
            dry_run=True,
        )
        # Run all the necessary instances
        sim.run()
