from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation, report, parameters as params
from collections import Counter
import logging
import math


class RabbitEnv(Environment):
    prob_death: params.probability = 1e-100

    def init(self):
        a1 = self.add_node(Male)
        a2 = self.add_node(Female)
        a1.add_edge(a2)
        self.add_agent(RandomAccident)

    @report
    @property
    def num_rabbits(self):
        return self.count_agents(agent_class=Rabbit)

    @report
    @property
    def num_males(self):
        return self.count_agents(agent_class=Male)

    @report
    @property
    def num_females(self):
        return self.count_agents(agent_class=Female)


class Rabbit(NetworkAgent, FSM):

    sexual_maturity = 30
    life_expectancy = 300

    @default_state
    @state
    def newborn(self):
        self.info("I am a newborn.")
        self.age = 0
        self.offspring = 0
        return self.youngling

    @state
    def youngling(self):
        self.age += 1
        if self.age >= self.sexual_maturity:
            self.info(f"I am fertile! My age is {self.age}")
            return self.fertile

    @state
    def fertile(self):
        raise Exception("Each subclass should define its fertile state")

    @state
    def dead(self):
        self.die()


class Male(Rabbit):
    max_females = 5
    mating_prob = 0.001

    @state
    def fertile(self):
        self.age += 1

        if self.age > self.life_expectancy:
            return self.dead

        # Males try to mate
        for f in self.model.agents(
            agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
        ):
            self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
            if self.prob(self["mating_prob"]):
                f.impregnate(self)
                break  # Take a break


class Female(Rabbit):
    gestation = 10
    pregnancy = -1

    @state
    def fertile(self):
        # Just wait for a Male
        self.age += 1
        if self.age > self.life_expectancy:
            return self.dead
        if self.pregnancy >= 0:
            return self.pregnant

    def impregnate(self, male):
        self.info(f"impregnated by {repr(male)}")
        self.mate = male
        self.pregnancy = 0
        self.number_of_babies = int(8 + 4 * self.random.random())

    @state
    def pregnant(self):
        self.info("I am pregnant")
        self.age += 1

        if self.age >= self.life_expectancy:
            return self.die()

        if self.pregnancy < self.gestation:
            self.pregnancy += 1
            return

        self.info("Having {} babies".format(self.number_of_babies))
        for i in range(self.number_of_babies):
            state = {}
            agent_class = self.random.choice([Male, Female])
            child = self.model.add_node(agent_class=agent_class, **state)
            child.add_edge(self)
            try:
                child.add_edge(self.mate)
                self.model.agents[self.mate].offspring += 1
            except ValueError:
                self.debug("The father has passed away")

            self.offspring += 1
        self.mate = None
        self.pregnancy = -1
        return self.fertile

    def die(self):
        if "pregnancy" in self and self["pregnancy"] > -1:
            self.info("A mother has died carrying a baby!!")
        return super().die()


class RandomAccident(BaseAgent):
    def step(self):
        rabbits_alive = self.model.G.number_of_nodes()

        if not rabbits_alive:
            return self.die()

        prob_death = self.model.prob_death * math.floor(
            math.log10(max(1, rabbits_alive))
        )
        self.debug("Killing some rabbits with prob={}!".format(prob_death))
        for i in self.get_agents(agent_class=Rabbit):
            if i.state_id == i.dead.id:
                continue
            if self.prob(prob_death):
                self.info("I killed a rabbit: {}".format(i.id))
                rabbits_alive -= 1
                i.die()
        self.debug("Rabbits alive: {}".format(rabbits_alive))



sim = Simulation(model=RabbitEnv, max_time=100, seed="MySeed", num_trials=1)

if __name__ == "__main__":
    sim.run()