from objectextensions import Extension

from typing import Iterable, FrozenSet
from abc import ABC

from ...renderables import Renderable
from ...hitboxes import Hitbox


class Hitboxed(Extension, ABC):
    @staticmethod
    def can_extend(target_cls):
        return issubclass(target_cls, Renderable)

    @staticmethod
    def extend(target_cls):
        Extension._wrap(target_cls, "__init__", Hitboxed.__wrap_init)

        Extension._set_property(target_cls, "hitboxes", Hitboxed.__hitboxes)
        Extension._set_setter(target_cls, "hitboxes", "hitboxes", Hitboxed.__set_hitboxes)

        Extension._set(target_cls, "generate_hitboxes", Hitboxed.__generate_hitboxes)
        Extension._set(target_cls, "check_collisions", Hitboxed.__check_collisions)
        Extension._set(target_cls, "collide", Hitboxed.__collide)

    def __wrap_init(self, *args, **kwargs):
        yield
        Extension._set(self, "_hitboxes", frozenset())
        self.hitboxes = self.generate_hitboxes()

    def __hitboxes(self) -> FrozenSet[Hitbox]:
        return self._hitboxes

    def __set_hitboxes(self, hitboxes: Iterable[Hitbox]):
        hitbox_handler = self.game.screen.hitbox_handler

        for hitbox in self._hitboxes:
            if hitbox not in hitboxes:
                hitbox_handler.remove(hitbox)

        for hitbox in hitboxes:
            if hitbox not in self._hitboxes:
                hitbox_handler.add(hitbox)

        self._hitboxes = frozenset(hitboxes)

    def __generate_hitboxes(self) -> Iterable[Hitbox]:
        """
        Must be overridden.
        Should return new, correct hitboxes for this Renderable object based on its current state.
        Note that hitboxes can still be created elsewhere on an ad-hoc basis (e.g. in response to specific events)
        """

        raise NotImplementedError

    def __check_collisions(self) -> None:
        """
        Can optionally be overridden.
        Standardises the entry point for checking collisions for this Renderable object.

        Hitboxes from other Renderable objects that may have valid collisions with this one's hitboxes can be retrieved
        using the get() method on self.game.screen.hitbox_handler, filtered down via the optional parameters
        that method can receive.

        When a collision is detected between two objects, each of their respective collide() methods should
        be invoked and passed the other object involved in that collision.
        """

        pass

    def __collide(self, other: "Renderable.with_extensions(Hitboxed)") -> None:
        """
        Can optionally be overridden.
        Should apply this renderable's on-collision effects to itself and/or the provided other renderable.

        This includes inspecting the other renderable (if needed) to decide what effects should apply.

        When a collision is detected and this method is invoked, it should *also* be invoked on the other object
        involved in the collision and passed this object as an argument
        to allow both objects' collision logic to be applied.

        # TODO: Review the below rule in practice - 'squishy' objects and 'immovable' objects
                    may want to edit the other object, or not be edited, respectively
        - Any *direct* changes to the position or movement (or other stats) of a renderable as a result of the collision
          itself should be handled by that renderable, not by the other involved renderable
        """

        pass
