<reponame>benthomasson/py_rete
from __future__ import annotations
from typing import TYPE_CHECKING
from itertools import chain
from dataclasses import dataclass

from py_rete.common import V

if TYPE_CHECKING:  # pragma: no cover
    from typing import List
    from typing import Union
    from typing import Tuple
    from typing import Callable
    from typing import Hashable
    from py_rete.common import WME


class ConditionalElement():
    """
    A single conditional element (e.g., Test, Condition, Neg, Bind). Does not
    include list conditionals. Used for type checking primarily.
    """
    pass


class ConditionalList(tuple):
    """
    A conditional that consists of a list of other conditionals.
    """
    def __new__(cls, *args: List[Union[ConditionalList, ConditionalElement]]):
        return super().__new__(cls, args)

    def __repr__(self):
        return "{}{}".format(self.__class__.__name__, super().__repr__())

    def __hash__(self):
        return hash((self.__class__.__name__, tuple(self)))


class ComposableCond:
    """
    A Mixin for making a conditional compositional using bitwise operators.
    """

    def __and__(self, other: ComposableCond):
        if isinstance(self, AND) and isinstance(other, AND):
            return AND(*[x for x in chain(self, other)])
        elif isinstance(self, AND):
            return AND(*[x for x in self]+[other])
        elif isinstance(other, AND):
            return AND(*[self]+[x for x in other])
        else:
            return AND(self, other)

    def __or__(self, other: ComposableCond):
        if isinstance(self, OR) and isinstance(other, OR):
            return OR(*[x for x in chain(self, other)])
        elif isinstance(self, OR):
            return OR(*[x for x in self]+[other])
        elif isinstance(other, OR):
            return OR(*[self]+[x for x in other])
        else:
            return OR(self, other)

    def __invert__(self):
        return NOT(self)


class AND(ConditionalList, ComposableCond):
    pass


class OR(ConditionalList, ComposableCond):
    pass


class NOT(ConditionalList, ComposableCond):
    pass


@dataclass(eq=True, frozen=True)
class Cond(ConditionalElement, ComposableCond):
    """
    Essentially a pattern/condition to match, can have variables.
    """
    __slots__ = ['identifier', 'attribute', 'value']
    identifier: Hashable
    attribute: Hashable
    value: Hashable

    def __repr__(self):
        return "(%s ^%s %s)" % (self.identifier, self.attribute, self.value)

    @property
    def vars(self) -> List[Tuple[str, V]]:
        """
        Returns a list of tuples (field, var) that contains the slot names as a
        string and the variable object it maps to.
        """
        return [(field, getattr(self, field))
                for field in ('identifier', 'attribute', 'value')
                if isinstance(getattr(self, field), V)]

    def contain(self, v: V) -> str:
        """
        Checks if a variable is in the condition. Returns as string with the
        name of the field if it is, otherwise an empty string.
        """
        assert isinstance(v, V)

        for f in ['identifier', 'attribute', 'value']:
            _v = getattr(self, f)
            if _v == v:
                return f
        return ""

    def test(self, w: WME) -> bool:
        """
        Checks if a pattern matches a working memory element.
        """
        for f in ['identifier', 'attribute', 'value']:
            v = getattr(self, f)
            if isinstance(v, V):
                continue
            if v != getattr(w, f):
                return False
        return True

    def __hash__(self):
        return hash(('cond', self.identifier, self.attribute, self.value))


class Neg(Cond):
    """
    A negated pattern.
    """
    def __repr__(self):
        return "-(%s ^%s %s)" % (self.identifier, self.attribute, self.value)

    def __hash__(self):
        return hash(('neg', self.identifier, self.attribute, self.value))


class Ncc(ConditionalList, ComposableCond):
    """
    A negated conjunction of conditions.
    """
    def __repr__(self):
        return "-{}".format(super(Ncc, self).__repr__())

    @property
    def number_of_conditions(self) -> int:
        return len(self)

    def __hash__(self):
        return hash(('ncc', tuple(self)))


@dataclass(eq=True, frozen=True)
class Filter(ConditionalElement, ComposableCond):
    """
    This is a test, it includes a function that might include variables.
    When employed in rete, the variable bindings are passed in as keyword args
    and the function is exectued. The function must return a boolean and if it
    evaluates to true, then the condition matches otherwise it does not.
    """
    __slots__ = ['func']
    func: Callable

    def __repr__(self):
        return "Filter({})".format(repr(self.func))

    def __hash__(self):
        return hash(('filter', self.func))


@dataclass(eq=True, frozen=True)
class Bind(ConditionalElement, ComposableCond):
    """
    Similar to Filter, but binds the result of a function execution to a new
    variable.
    """
    __slots__ = ['func', 'to']
    func: Callable
    to: V

    def __repr__(self):
        return "Bind({},{})".format(repr(self.func), repr(self.to))

    def __hash__(self):
        return hash(('bind', self.func, self.to))
