import random
import json
from sqlalchemy import Boolean
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql.expression import cast

from dallinger.models import Info
from dallinger.models import Transformation
from dallinger.nodes import Agent
from dallinger.nodes import Source


class MCMCPAgent(Agent):

    __mapper_args__ = {"polymorphic_identity": "MCMCP_agent"}

    def update(self, infos):
        info = infos[0]
        self.replicate(info)
        new_info = AnimalInfo(origin=self, contents=info.perturbed_contents())
        Perturbation(info_in=info, info_out=new_info)

    def _what(self):
        infos = self.infos()
        return [i for i in infos if i.chosen][0]


class AnimalSource(Source):
    """A source that transmits animal shapes."""

    __mapper_args__ = {"polymorphic_identity": "animal_source"}

    def create_information(self):
        """Create a new Info.

        transmit() -> _what() -> create_information().
        """
        return AnimalInfo(origin=self, contents=None)


class AnimalInfo(Info):
    """An Info that can be chosen."""

    __mapper_args__ = {"polymorphic_identity": "vector_info"}

    @hybrid_property
    def chosen(self):
        """Use property1 to store whether an info was chosen."""
        try:
            return bool(self.property1)
        except TypeError:
            return None

    @chosen.setter
    def chosen(self, chosen):
        """Assign chosen to property1."""
        self.property1 = repr(chosen)

    @chosen.expression
    def chosen(self):
        """Retrieve chosen via property1."""
        return cast(self.property1, Boolean)

    properties = {
        "foot_spread": [0, 1],
        "body_height": [0.1, 1.5],
        "body_tilt": [-15, 45],
        "tail_length": [0.05, 1.2],
        "tail_angle": [-45, 190],
        "neck_length": [0, 2.5],
        "neck_angle": [90, 180],
        "head_length": [0.05, 0.75],
        "head_angle": [5, 80],
    }

    def __init__(self, origin, contents=None, **kwargs):
        if contents is None:
            data = {}
            for prop, prop_range in self.properties.items():
                data[prop] = random.uniform(prop_range[0], prop_range[1])
            contents = json.dumps(data)

        super(AnimalInfo, self).__init__(origin, contents, **kwargs)

    def perturbed_contents(self):
        """Perturb the given animal."""
        animal = json.loads(self.contents)

        for prop, prop_range in self.properties.items():
            range = prop_range[1] - prop_range[0]
            jittered = animal[prop] + random.gauss(0, 0.1 * range)
            animal[prop] = max(min(jittered, prop_range[1]), prop_range[0])

        return json.dumps(animal)


class Perturbation(Transformation):
    """A perturbation is a transformation that perturbs the contents."""

    __mapper_args__ = {"polymorphic_identity": "perturbation"}