"""
`EvoLife`_ ported to Xentica.

.. _EvoLife: https://github.com/a5kin/evolife

"""
from xentica import core
from xentica import seeds
from xentica.tools import xmath
from xentica.core import color_effects
from xentica.tools.color import GenomeColor
from xentica.tools.genetics import genome_crossover
from xentica.tools.rules import LifeLike
from xentica.seeds.random import RandInt

from examples.base import RegularCA, RegularExperiment
from examples.base import run_simulation


class EvoLife(RegularCA):
    """
    Life-like cellular automaton with evolutionary rules for each cell.

    Rules are:
    - Each living cell has its own birth/sustain ruleset and an energy
      level;
    - Cell is loosing all energy if number of neighbours is not in its
      sustain rule;
    - Cell is born with max energy if there are exactly N neighbours
      with N in their birth rule;
        - Same is applied for living cells (re-occupation case), if
          new genome is different;
    - If there are several birth situations with different N possible,
      we choose one with larger N;
    - Newly born cell's ruleset calculated as crossover between
      'parent' cells rulesets;
    - Every turn, cell is loosing DEATH_SPEED units of energy;
    - Cell with zero energy is dying;
    - Cell cannot have more than MAX_GENES non-zero genes in ruleset.

    """
    energy = core.IntegerProperty(max_val=255 * 2)
    rule = core.TotalisticRuleProperty(outer=True)
    rng = core.RandomProperty()
    death_speed = core.Parameter(default=15)
    max_genes = core.Parameter(default=9)
    mutation_prob = core.Parameter(default=.0)

    def __init__(self, *args, legacy_coloring=True):
        """Support legacy coloring as needed."""
        self._legacy_coloring = legacy_coloring
        super().__init__(*args)

    def emit(self):
        """Broadcast the state to all neighbors."""
        for i in range(len(self.buffers)):
            self.buffers[i].energy = self.main.energy
            self.buffers[i].rule = self.main.rule

    def absorb(self):
        """Apply EvoLife dynamics."""
        # test if cell is sustained
        num_neighbors = core.IntegerVariable()
        for i in range(len(self.buffers)):
            nbr_energy = self.neighbors[i].buffer.energy
            nbr_rule = self.neighbors[i].buffer.rule
            num_neighbors += xmath.min(1, (nbr_energy + nbr_rule))
        is_sustained = core.IntegerVariable()
        is_sustained += self.main.rule.is_sustained(num_neighbors)

        # test if cell is born
        fitnesses = []
        for i in range(len(self.buffers)):
            fitnesses.append(core.IntegerVariable(name="fit%d" % i))
        num_parents = core.IntegerVariable()
        for gene in range(len(self.buffers)):
            num_parents *= 0  # hack for re-init variable
            for i in range(len(self.buffers)):
                nbr_energy = self.neighbors[i].buffer.energy
                nbr_rule = self.neighbors[i].buffer.rule
                is_alive = xmath.min(1, (nbr_energy + nbr_rule))
                is_fit = self.neighbors[i].buffer.rule.is_born(gene + 1)
                num_parents += is_alive * is_fit
            fitnesses[gene] += num_parents * (num_parents == (gene + 1))
        num_fit = core.IntegerVariable()
        num_fit += xmath.max(*fitnesses)

        # neighbor's genomes crossover
        genomes = []
        for i in range(len(self.buffers)):
            genomes.append(core.IntegerVariable(name="genome%d" % i))
        for i in range(len(self.buffers)):
            is_fit = self.neighbors[i].buffer.rule.is_born(num_fit)
            genomes[i] += self.neighbors[i].buffer.rule * is_fit
        num_genes = self.main.rule.bit_width
        old_rule = core.IntegerVariable()
        old_rule += self.main.rule
        old_energy = core.IntegerVariable()
        old_energy += self.main.energy
        new_genome = genome_crossover(
            self.main, num_genes, *genomes,
            max_genes=self.meta.max_genes,
            mutation_prob=self.meta.mutation_prob
        )
        self.main.rule = new_genome + self.main.rule * (new_genome == 0)

        # new energy value
        self.main.energy *= 0
        is_live = core.IntegerVariable()
        old_live = (old_energy + old_rule) == 0
        is_live += (old_energy < 0xff) & (old_live | is_sustained)
        old_dead = (old_energy + old_rule) != 0
        new_energy = old_energy + self.meta.death_speed * old_dead
        self.main.energy = new_energy * (self.main.rule == old_rule) + \
            self.main.energy * (self.main.rule != old_rule)
        self.main.rule = old_rule * (self.main.rule == old_rule) + \
            self.main.rule * (self.main.rule != old_rule)
        self.main.energy *= is_live
        self.main.rule *= is_live

    @color_effects.MovingAverage
    def color(self):
        """Render cell's genome as hue/sat, cell's energy as value."""
        if self._legacy_coloring:
            red, green, blue = GenomeColor.modular(self.main.rule >> 1, 360)
        else:
            red, green, blue = GenomeColor.positional(self.main.rule,
                                                      self.main.rule.bit_width)
        is_live = (self.main.rule > 0) * (self.main.energy < 255)
        energy = (255 - self.main.energy) * is_live
        red = xmath.int(red * energy)
        green = xmath.int(green * energy)
        blue = xmath.int(blue * energy)
        return (red, green, blue, )


class CrossbreedingExperiment(RegularExperiment):
    """Classic experiment for legacy EvoLife, where 'Bliambas' may form."""

    word = "FUTURE BREEDING MACHINE 2041"
    size = (1920, 1080)
    zoom = 1
    fade_in = 18
    fade_out = 3
    seed_diamoeba = seeds.patterns.BigBang(
        pos=(380, 280),
        size=(440, 340),
        vals={
            "energy": 0,
            "rule": LifeLike.golly2int("B35678/S5678") * RandInt(0, 1),
            "rng": RandInt(0, 2 ** 16 - 1)
        }
    )
    seed_conway = seeds.patterns.BigBang(
        pos=(0, 0),
        size=(1280, 720),
        vals={
            "energy": 0,
            # you may use unary operators
            "rule": +RandInt(0, 1) * LifeLike.golly2int("B3/S23"),
            # as well as reflected expressions
            "rng": 0 + RandInt(0, 2 ** 16 - 1)
        }
    )
    seed_rng = seeds.patterns.PrimordialSoup(
        vals={
            "rng": RandInt(0, 2 ** 16 - 1)
        }
    )
    # chain ordering matters, since areas are rewriting each other in order
    seed = seed_rng + seed_conway + seed_diamoeba


class CrossbreedingExperiment2(CrossbreedingExperiment):
    """Same crossbreeding experiment with different meta-parameters."""
    death_speed = 25
    max_genes = 9
    mutation_prob = 0.0001


class BigBangExperiment(RegularExperiment):
    """Experiment, with smaller field, for quicker benchmark."""
    death_speed = 0
    max_genes = 9
    seed_bang = seeds.patterns.BigBang(
        pos=(0, 0),
        size=(100, 100),
        vals={
            "energy": RandInt(0, 255),
            "rule": RandInt(0, 2 ** 18 - 1) & 0b111111110111111110,
            "rng": RandInt(0, 2 ** 16 - 1)
        }
    )
    seed_rng = seeds.patterns.PrimordialSoup(
        vals={
            "rng": RandInt(0, 2 ** 16 - 1)
        }
    )
    seed = seed_rng + seed_bang


class EvoLife6D(EvoLife):
    """EvoLife variant in 6D"""

    class Topology(EvoLife.Topology):
        """Same topology as 6D Life."""
        dimensions = 6
        neighborhood = core.VonNeumannNeighborhood()


class EvoLife6DExperiment(RegularExperiment):
    """
    Generic experiment for 6D EvoLife: 2 spatial, 4 micro-dimensions.

    WARNING: to run this model you need at least 9GB GPU RAM free.

    """

    word = "SP1RALS 1N HYPERSP@CE"
    size = (640, 380, 7, 5, 3, 3)
    pos = [50, 50, 0, 0, 0, 0]
    death_speed = 64
    max_genes = 13
    mutation_prob = 0.0001
    fade_in = 63
    fade_out = 18
    mask_random = RandInt(0, 2 ** 26 - 1, constant=True)
    seed_bang = seeds.patterns.BigBang(
        pos=(0, 0, 0, 0, 0, 0),
        size=(100, 100, 1, 1, 1, 1),
        vals={
            "energy": RandInt(0, 1) * 255,
            "rule": RandInt(0, 2 ** 26 - 1) & mask_random,
            "rng": RandInt(0, 2 ** 16 - 1)
        }
    )
    seed_rng = seeds.patterns.PrimordialSoup(
        vals={
            "rng": RandInt(0, 2 ** 16 - 1)
        }
    )
    seed = seed_rng + seed_bang


if __name__ == "__main__":
    run_simulation(EvoLife, CrossbreedingExperiment2)
