from peak.rules.core import *
from peak.rules.core import sorted, frozenset, set
from peak.util.decorators import struct
from weakref import ref
from sys import maxint
from peak.util.extremes import Min, Max
__all__ = [
    'Range', 'Value', 'IsObject', 'Class', 'Classes', 'tests_for',
    'NotObjects',  'Conjunction', 'Disjunction', 'Test', 'Signature',
    'Inequality', 'DisjunctionSet', 'OrElse',
]

class Intersection(object):
    """Abstract base for conjunctions and signatures"""
    __slots__ = ()

class Disjunction(object):
    """Abstract base for DisjunctionSet and OrElse
    
    Note that a Disjunction can never have less than 2 members, as creating a
    Disjunction with only 1 item returns that item, and creating one with no
    items returns ``False`` (as no acceptable conditions means "never true").
    """
    __slots__ = ()

    def __new__(cls, input):
        if cls is Disjunction:
            return DisjunctionSet(input)
        return super(Disjunction, cls).__new__(cls)

when(negate, (Disjunction,))(lambda c: reduce(intersect, map(negate, c)))
        
struct()
def Range(lo=(Min,-1), hi=(Max,1)):
    if hi<=lo: return False
    return lo, hi

struct()
def Value(value, match=True):
    return value, match

when(implies, (Value, Range))(
    # ==Value implies Range if Value is within range
    lambda c1, c2: c1.match and c2.lo <= (c1.value, 0) <= (c2.hi)
)
when(implies, (Range, Value))(
    # Range implies !=Value if Value is out of range
    lambda c1, c2: not c2.match and not (c1.lo <= (c2.value, 0) <= (c1.hi)) or
                   c2.match and c1==Range((c2.value,-1), (c2.value,1))
)
when(implies, (Range, Range))(
    # Range1 implies Range2 if both points are within Range2's bounds
    lambda c1, c2: c1.lo >= c2.lo and c1.hi <= c2.hi
)
when(implies, (Value, Value))(
    lambda c1, c2: c1==c2 or (c1.match and not c2.match and c1.value!=c2.value)
)
when(intersect, (Range, Range))(
    lambda c1, c2: Range(max(c1.lo, c2.lo), min(c1.hi, c2.hi))
)

def to_range(v):
    lo, hi = (v.value, -1), (v.value, 1)
    if v.match:
        return Range(lo, hi)
    return Disjunction([Range(hi=lo), Range(lo=hi)])

when(intersect, (Value, Value))
def intersect_values(c1, c2):
    if c1==c2: return c1
    return intersect(to_range(c1), to_range(c2))

# if a value is a point inside a range, the implication rules would handle it;
# therefore, they are either !=values, or outside the range (and thus empty)
when(intersect, (Range, Value))(
    lambda c1, c2: not c2.match and intersect(c1, to_range(c2)) or False
)
when(intersect, (Value, Range))(
    lambda c1, c2: not c1.match and intersect(to_range(c1), c2) or False
)


when(negate, (Value,))(lambda c: Value(c.value, not c.match))
when(negate, (Range,))(lambda c: Disjunction([Range(hi=c.lo), Range(lo=c.hi)]))


struct()
def Class(cls, match=True):
    return cls, match

when(negate, (Class,))(lambda c: Class(c.cls, not c.match))

when(implies, (Class, Class))
def class_implies(c1, c2):
    if c1==c2:
        # not isinstance(x) implies not isinstance(x) always
        #     isinstance(x) implies isintance(x)      always
        return True
    elif c1.match and c2.match:
        #     isinstance(x) implies     isinstance(y) if issubclass(x,y)
        return implies(c1.cls, c2.cls)
    elif c1.match or c2.match:
        # not isinstance(x) implies     isinstance(x) never
        #     isinstance(x) implies not isinstance(y) never
        return False
    else:
        # not isinstance(x) implies not isinstance(y) if issubclass(y, x)
        return implies(c2.cls, c1.cls)

when(intersect, (Class, Class))(lambda c1,c2: Classes([c1, c2]))

when(implies, (istype, Class))(lambda c1,c2:
    c1.match and (c2.match == issubclass(c1.type, c2.cls))
)
when(implies, (Class, istype))(lambda c1,c2:
    c1.match and not c2.match and c1.cls is not c2.type
    and issubclass(c1.cls, c2.type)
)





struct()
def Test(expr, criterion):
    d = disjuncts(criterion)
    if len(d)!=1:
        return Disjunction([Test(expr, c) for c in d])
    return expr, criterion

when(negate, (Test,))(lambda c: Test(c.expr, negate(c.criterion)))

when(implies, (Test, Test))(
    lambda c1, c2: c1.expr==c2.expr and implies(c1.criterion, c2.criterion)
)

when(intersect, (Test, Test))
def intersect_tests(c1, c2):
    if c1.expr==c2.expr:
        return Test(c1.expr, intersect(c1.criterion, c2.criterion))
    else:
        return Signature([c1, c2])

inequalities = {
    '>':  lambda v: Range(lo=(v, 1)),
    '>=': lambda v: Range(lo=(v,-1)),
    '<':  lambda v: Range(hi=(v,-1)),
    '<=': lambda v: Range(hi=(v, 1)),
    '!=': lambda v: Value(v, False),
    '==': lambda v: Value(v, True),
}

inequalities['=<'] = inequalities['<=']
inequalities['=>'] = inequalities['>=']
inequalities['<>'] = inequalities['!=']

def Inequality(op, value):
    return inequalities[op](value)






class Signature(Intersection, tuple):
    """Represent and-ed Tests, in sequence"""

    def __new__(cls, input):
        output = []
        index = {}
        input = iter(input)
        for new in input:
            if new is True:
                continue
            elif new is False:
                return False
            assert isinstance(new, Test), \
                "Signatures can only contain ``criteria.Test`` instances"
            expr = new.expr
            if expr in index:
                posn = index[expr]
                old = output[posn]
                if implies(old, new):
                    continue    # 'new' is irrelevant, skip it
                new = output[index[expr]] = intersect(old, new)
                if new is False: return False
            else:
                posn = index[expr] = len(output)
                output.append(new)

        if not output:
            return True
        elif len(output)==1:
            return output[0]
        return tuple.__new__(cls, output)

    def __repr__(self):
        return "Signature("+repr(list(self))+")"

when(negate, (Signature,))(lambda c: OrElse(map(negate, c)))





class IsObject(int):
    """Criterion for 'is' comparisons"""

    __slots__ = 'ref', 'match'

    def __new__(cls, ob, match=True):
        self = IsObject.__base__.__new__(cls, id(ob)&maxint)
        self.match = match
        self.ref = ob
        return self

    def __eq__(self,other):
        return self is other or (
            isinstance(other, IsObject) and self.match==other.match
            and self.ref is other.ref
        ) or (isinstance(other,(int,long)) and int(self)==other and self.match)

    def __repr__(self):
        return "IsObject(%r, %r)" % (self.ref, self.match)

when(negate, (IsObject,))(lambda c: IsObject(c.ref, not c.match))

when(implies, (IsObject, IsObject))
def implies_objects(c1, c2):
    # c1 implies c2 if it's identical, or if c1=="is x" and c2=="is not y"
    return c1==c2 or (c1.match and not c2.match and c1.ref is not c2.ref)

when(intersect, (IsObject, IsObject))
def intersect_objects(c1, c2):
    #  'is x and is y'            'is not x and is x'
    if (c1.match and c2.match) or (c1.ref is c2.ref):
        return False
    else:
        # "is not x and is not y"
        return NotObjects([c1,c2])






class DisjunctionSet(Disjunction, frozenset):
    """Set of minimal or-ed conditions (i.e. no redundant/implying items)

    Note that a Disjunction can never have less than 2 members, as creating a
    Disjunction with only 1 item returns that item, and creating one with no
    items returns ``False`` (as no acceptable conditions means "never true").
    """
    def __new__(cls, input):
        output = []
        for item in input:
            for new in disjuncts(item):
                for old in output[:]:
                    if implies(new, old):
                        break
                    elif implies(old, new):
                        output.remove(old)
                else:
                    output.append(new)
        if not output:
            return False
        elif len(output)==1:
            return output[0]
        return frozenset.__new__(cls, output)

when(implies, (Disjunction, object))
when(implies, (Disjunction, Disjunction))
def union_implies(c1, c2):  # Or(...) implies x if all its disjuncts imply x
    for c in c1:
        if not implies(c, c2):
            return False
    else:
        return True

when(implies, (object, Disjunction))
def ob_implies_union(c1, c2):   # x implies Or(...) if it implies any disjunct
    for c in c2:
        if implies(c1, c):
            return True
    else:
        return False

# We use @around for these conditions in order to avoid redundant implication
# testing during intersection, as well as to avoid ambiguity with the
# (object, bool) and  (bool, object) rules for intersect().
#
around(intersect, (Disjunction, object))(
    lambda c1, c2: type(c1)([intersect(x,c2) for x in c1])
)
around(intersect, (object, Disjunction))(
    lambda c1, c2: type(c2)([intersect(c1,x) for x in c2])
)
around(intersect, (Disjunction, Disjunction))(
    lambda c1, c2: type(c1)([intersect(x,y) for x in c1 for y in c2])
)
around(intersect, (Disjunction, Intersection))(
    lambda c1, c2: type(c1)([intersect(x,c2) for x in c1])
)
around(intersect, (Intersection, Disjunction))(
    lambda c1, c2: type(c2)([intersect(c1,x) for x in c2])
)

# XXX These rules prevent ambiguity with implies(object, bool) and
# (bool, object), at the expense of redundancy.  This can be cleaned up later
# when we allow cloning of actions for an existing rule.  (That is, when we can
# say "treat (bool, Disjunction) like (bool, object)" without duplicating the
# action.)
#
when(implies, (bool, Disjunction))(lambda c1, c2: not c1)
when(implies, (Disjunction, bool))(lambda c1, c2: c2)

# The disjuncts of a Disjunction are a list of its contents:
when(disjuncts, (DisjunctionSet,))(list)

abstract()
def tests_for(ob, engine=None):
    """Yield the tests composing ob, if any"""

when(tests_for, (Test,     ))(lambda ob, e: iter([ob]))
when(tests_for, (Signature,))(lambda ob, e: iter(ob))
when(tests_for, (bool,     ))(lambda ob, e: iter([]))


class Conjunction(Intersection, frozenset):
    """Set of minimal and-ed conditions (i.e. no redundant/implied items)

    Note that a Conjunction can never have less than 2 members, as
    creating a Conjunction with only 1 item returns that item, and
    creating one with no items returns ``True`` (since no required conditions
    means "always true").
    """
    def __new__(cls, input):
        output = []
        for new in input:
            for old in output[:]:
                if implies(old, new):
                    break
                elif implies(new, old):
                    output.remove(old)
                elif mutually_exclusive(new, old):
                    return False
            else:
                output.append(new)
        if not output:
            return True
        elif len(output)==1:
            return output[0]
        return frozenset.__new__(cls, output)

around(implies, (Intersection, object))
def set_implies(c1, c2):
    for c in c1:
        if implies(c, c2): return True
    else:
        return False

around(implies, (object, Intersection))
around(implies, (Intersection, Intersection))
def ob_implies_set(c1, c2):
    for c in c2:
        if not implies(c1, c): return False
    else:
        return True

when(negate, (Conjunction,))(lambda c: Disjunction(map(negate, c)))

when(intersect, (istype,Class))
def intersect_type_class(c1, c2):
    if not c1.match: return Classes([c1,c2])
    return False

when(intersect, (Class, istype))
def intersect_type_class(c1, c2):
    if not c2.match: return Classes([c1,c2])
    return False

when(intersect, (istype,istype))
def intersect_type_type(c1, c2):
    # This is only called if there's no implication
    if c1.match or c2.match:
        return False    # so unless both are False matches, it's an empty set
    return Classes([c1, c2])

def mutually_exclusive(c1, c2):
    """Is the intersection of c1 and c2 known to be always false?"""
    return False

when(mutually_exclusive, (istype, istype))(
    lambda c1, c2: c1.match != c2.match and c1.type==c2.type
)















# Intersecting an intersection with something else should return a set of the
# same type as the leftmost intersection.  These methods are declared @around
# to avoid redundant implication testing during intersection, as well as to
# avoid ambiguity with the (object, bool) and  (bool, object) rules for
# intersect().
#
around(intersect, (Intersection, Intersection))(
    lambda c1, c2: type(c1)(list(c1)+list(c2))
)
around(intersect, (Intersection, object))(
    lambda c1, c2: type(c1)(list(c1)+[c2])
)
around(intersect, (object, Intersection))(
    lambda c1, c2: type(c2)([c1]+list(c2))
)

# Default intersection is a Conjunction
when(intersect, (object, object))(lambda c1, c2: Conjunction([c1,c2]))

class Classes(Conjunction):
    """A set of related Class instances"""
    if __debug__:
        def __init__(self, input):
            for item in self:
                assert isinstance(item, (Class, istype)), (
                    "Classes() items must be ``criteria.Class``" 
                    " or ``istype`` instances")

class NotObjects(Conjunction):
    """Collection of and-ed "is not" conditions"""
    if __debug__:
        def __init__(self, input):
            for item in self:
                assert isinstance(item, IsObject) and not item.match, \
                    "NotObjects() items must be false ``IsObject`` instances"






class OrElse(Disjunction, tuple):
    """SEQUENCE of or-ed conditions (excluding redundant/implying items)"""

    def __new__(cls, input):
        output = []
        for item in input:
            for old in output[:]:
                if implies(item, old):
                    break
                elif implies(old, item):
                    output.remove(old)
            else:
                output.append(item)
        if not output:
            return False
        elif len(output)==1:
            return output[0]
        return tuple.__new__(cls, output)

    def __repr__(self):
        return "%s(%s)" % (self.__class__.__name__, list(self))


when(disjuncts, (OrElse,))
def sequential_disjuncts(c):
    pre = True
    out = set()
    for cond in c:
        out.update(disjuncts(intersect(pre, cond)))
        pre = intersect(pre, negate(cond))
    return list(out)










