import copy
from collections import namedtuple
from enum import auto, Enum
from typing import Set

"""Anatomical model for Voidstar."""

CapabilityRule = namedtuple(
    'CapabilityRule',
    [
        'predicate',
        'capability'
    ]
)

SymmetryInfo = namedtuple(
    'SymmetryInfo',
    [
        'type',
        'count'
    ]
)


class Capability(Enum):
    LIVE = auto()
    GRASPING = auto()
    WALKING = auto()


class Symmetry(Enum):
    BILATERAL = auto()
    RADIAL = auto()
    ASYMMETRIC = auto()
    SPHERICAL = auto()


class BodyPart:
    """Represent a node in the anatomical tree of an organism."""

    def __init__(self, name):
        self.name = name
        self.prefix = None
        self.attached = set()
        self.inner = set()
        self.covering = set()
        self.capability_rules = set()
        self.paper_doll_slots = []
        self.symmetry = SymmetryInfo(Symmetry.ASYMMETRIC, 0)
        self.material = "organic material"
        self.slot = None

    @staticmethod
    def _add_to_set(set_to_receive, body_parts):
        for part in body_parts:
            set_to_receive.add(part)

    def _get_symmetrical_copies(self, body_part):
        if self.symmetry.type is Symmetry.BILATERAL:
            left_part = copy.deepcopy(body_part).prepend_to_name('left')
            right_part = copy.deepcopy(body_part).prepend_to_name('right')
            return [left_part, right_part]
        elif (
                self.symmetry.type is Symmetry.RADIAL or
                self.symmetry.type is Symmetry.SPHERICAL
        ):
            output = []
            count = self.symmetry.count
            for i in range(count):
                part = copy.deepcopy(body_part)
                output.append(part)
            return output
        else:
            return [body_part]

    def prepend_to_name(self, prefix):
        """Prepend the given prefix to the existing name and prefix of this part and all children."""
        self.name = '{0} {1}'.format(prefix, self.name)
        self.prefix = '{0} {1}'.format(prefix, self.prefix) if self.prefix else prefix
        for part in self.attached:
            part.prepend_to_name(prefix)
        for part in self.inner:
            part.prepend_to_name(prefix)
        return self

    def add_attached(self, *body_parts, ignore_symmetry=False):
        """Add a body part attached to this body part."""
        if ignore_symmetry:
            self._add_to_set(self.attached, body_parts)
        else:
            for body_part in body_parts:
                self._add_to_set(
                    self.attached,
                    self._get_symmetrical_copies(body_part)
                )
        return self

    def add_center_attached(self, *body_parts):
        """Add a part, ignoring symmetry."""
        self.add_attached(*body_parts, ignore_symmetry=True)
        return self

    def add_inner(self, *body_parts, ignore_symmetry=False):
        """Add a body part inside of this body part."""
        if ignore_symmetry:
            self._add_to_set(self.inner, body_parts)
        else:
            for body_part in body_parts:
                self._add_to_set(
                    self.inner,
                    self._get_symmetrical_copies(body_part)
                )
        return self

    def add_center_inner(self, *body_parts):
        return self.add_inner(*body_parts, ignore_symmetry=True)

    def add_covering(self, *body_parts):
        """Add a covering (eg. skin) to this body part."""
        self._add_to_set(self.covering, body_parts)
        return self

    def add_capability(self, *rules: CapabilityRule):
        self._add_to_set(self.capability_rules, rules)
        return self

    def add_covering_to_all(self, *body_parts, exclude=None):
        """Add a covering to all attached parts, except for parts matching the exclude strings."""
        if isinstance(exclude, list):
            for exclusion in exclude:
                if exclusion in self.name:
                    return self
        elif exclude and exclude in self.name:
            return self
        self.add_covering(*body_parts)
        for body_part in self.attached:
            body_part.add_covering_to_all(exclude=exclude, *body_parts)
        return self

    def get_flat_attached(self):
        output = [self.name]
        for part in self.attached:
            output += part.get_flat_attached()
        return output

    def get_all_body_parts(self):
        output = [self.name]
        for parts in [
            self.attached,
            self.inner,
            self.covering
        ]:
            for part in parts:
                output += part.get_all_body_parts()
        return output

    def set_symmetry(self, symmetry_type=None, count=0):
        self.symmetry = SymmetryInfo(symmetry_type, count)
        return self

    def set_material(self, material):
        self.material = material
        return self

    def remove(self, matching_name):
        """Remove parts with names that contain the matching_name."""
        for parts in [
            self.attached,
            self.inner,
            self.covering
        ]:
            parts_to_remove = []
            for part in parts:
                if matching_name in part.name:
                    parts_to_remove.append(part)
                else:
                    part.remove(matching_name)
            for part in parts_to_remove:
                parts.remove(part)
        return self

    def replace(self, matching_name, body_part):
        """
        Recursively replace body parts with a matching name with equivalent body_part.

        Will only go as deep in a branch of the anatomical tree as the first match, i.e.
        if five fingers are attached to one finger, replace('finger', tentacle) will replace
        the top-level finger with a tentacle, removing all fingers in the process.
        """

        for parts in [
            self.attached,
            self.inner,
            self.covering
        ]:
            parts_to_remove = []
            parts_to_add = []
            for part in parts:
                if matching_name in part.name:
                    parts_to_remove.append(part)
                    new_part = copy.deepcopy(body_part)
                    if part.prefix:
                        new_part.prepend_to_name(part.prefix)
                    parts_to_add.append(
                        new_part
                    )
                else:
                    part.replace(matching_name, body_part)
            for part in parts_to_add:
                parts.add(part)
            for part in parts_to_remove:
                parts.remove(part)
        return self

    def has(self, n: int, name: str):
        return sum([1 for part in self.get_all_body_parts() if name in part]) >= n

    def get_capabilities(self) -> Set:
        output = set()
        for rule in self.capability_rules:
            if rule.predicate(self):
                output.add(rule.capability)
        for col in [
            self.attached,
            self.inner,
            self.covering,
        ]:
            for part in col:
                output.union(part.get_capabilities())
        return output

    def print(self, tabs=0):
        print("\t" * tabs + "{0} (made of {1})".format(self.name, self.material))
        for pair in [
            ('Attached', self.attached),
            ('Inner', self.inner),
            ('Covering', self.covering)
        ]:
            if pair[1]:
                print("\t" * (tabs + 1) + "{0}:".format(pair[0]))
                for part in pair[1]:
                    part.print(tabs=tabs + 2)


def humanoid_head():
    return BodyPart('head').set_symmetry(
        Symmetry.BILATERAL
    ).add_center_attached(
        BodyPart('face').set_symmetry(
            Symmetry.BILATERAL
        ).add_attached(
            BodyPart('eye'),
            BodyPart('ear')
        ).add_center_attached(
            BodyPart('nose'),
            BodyPart('mouth')
        )
    ).add_center_inner(
        BodyPart('skull').set_material(
            'bone'
        ).add_inner(
            BodyPart('brain')
        )
    )


def humanoid_leg():
    return BodyPart('upper leg').add_attached(
        BodyPart('lower leg').add_attached(
            BodyPart('foot').add_attached(
                BodyPart('big toe'),
                BodyPart('second toe'),
                BodyPart('third toe'),
                BodyPart('fourth toe'),
                BodyPart('small toe')
            )
        )
    )


def humanoid_arm():
    return BodyPart('upper arm').add_attached(
        BodyPart('lower arm').add_attached(
            BodyPart('hand').add_attached(
                BodyPart('thumb'),
                BodyPart('first finger'),
                BodyPart('second finger'),
                BodyPart('third finger'),
                BodyPart('fourth finger')
            )
        )
    )


def humanoid_trunk():
    return BodyPart('trunk').set_symmetry(
        symmetry_type=Symmetry.BILATERAL
    ).add_attached(
        humanoid_arm()
    ).add_attached(
        humanoid_leg()
    ).add_center_attached(
        humanoid_head()
    ).add_covering_to_all(
        BodyPart('skin'),
        exclude=['eye', 'mouth']
    ).add_capability(
        CapabilityRule(lambda x: x.has(1, 'brain') and x.has(1, 'trunk'), Capability.LIVE),
        CapabilityRule(lambda x: x.has(2, 'foot'), Capability.WALKING),
        CapabilityRule(lambda x: x.has(1, 'hand'), Capability.GRASPING)
    )


if __name__ == '__main__':
    print(humanoid_trunk().get_all_body_parts())
