from equibel.simbool.proposition import Prop

__falseProp = Prop(False)
__trueProp = Prop(True)

def simplify_term(P):
    """negation-atom: ~True -> False; ~False -> True
    negation-elimination: ~~Prop -> Prop
    Unary op-elimination: (|Prop) -> Prop; (&Prop) -> Prop
    false-disjunction: False | Prop -> Prop
    true-disjunction: True | Prop -> True
    false-conjunction: False & Prop -> False
    true-conjunction: True & Prop -> Prop
    opposite-elimination-conjunction: ~Prop1 | Prop2 -> True if Prop1 == Prop2
    opposite-elmination-disjunction: ~Prop1 & Prop2 -> False if Prop1 == Prop2"""
    if P.atomic:
        return P
    
    if P.get_op() == '~': 
        sub = P.get_terms()[0]
        if sub.atomic:
            if sub.name == True:
                return __falseProp
            if sub.name == False:
                return __trueProp
        if sub.get_op() == '~':
            return sub.get_terms()[0]
        return P
    
    if len(P.get_terms()) == 1:
        return P.get_terms()[0]

    def breakdown(P):
        positive = set()
        negative = set()
        negative_with_negation = set()
        for e in P.get_terms():
            if e.is_positive():
                positive.add(e)
            else:
                assert(e.get_op() == '~')
                negative.add(e.get_terms()[0])
                negative_with_negation.add(e)
        return positive, negative, negative_with_negation
    
    if P.get_op() == '&':
        pos, neg, negn = breakdown(P)

        if __falseProp in pos:
            return __falseProp

        if len(pos & neg) > 0:
            return __falseProp
        
        if __trueProp in pos:
            if len(pos) > 1 or len(neg) > 0:
                pos.remove(__trueProp)
            else:
                return __trueProp
        terms = list(pos)+list(negn)
        if len(terms) == 1:
            return terms[0]
        return Prop('&', *terms)

    if P.get_op() == '|':
        pos, neg, negn = breakdown(P)

        if __trueProp in pos:
            return __trueProp

        if len(pos & neg) > 0:
            return __trueProp
        
        if __falseProp in pos:
            if len(pos) > 1 or len(neg) > 0:
                pos.remove(__falseProp)
            else:
                return __falseProp
        
        terms = list(pos)+list(negn)
        if len(terms) == 1:
            return terms[0]
        
        return Prop('|', *terms)
    
    assert(False)

def associative_collect(P):
    return __associative_collect(P)[0]

def __associative_collect(P, sym = None):
    if P.is_literal():
        return [P]

    if P.get_op() == '~':
        return [Prop('~', *__associative_collect(P.get_terms()[0]))]

    if sym != P.get_op():
        col = []
        for e in P.get_terms():
            col += __associative_collect(e, P.get_op())
        return [Prop(P.get_op(), *col)]
    else:
        col = []
        for e in P.get_terms():
            r = __associative_collect(e, sym)
            col += r
        return col
    
    assert(False)

def simplify_everywhere(P):
    res = __simplify_everywhere(P)
    res_old = res
    while True:
        res = __simplify_everywhere(res)
        if res_old == res:
            break
        res_old = res
    return res

def __simplify_everywhere(P):
    if P.atomic:
        return P
    
    simplified = simplify_term(P)

    if simplified.atomic:
        return simplified
    
    return Prop(simplified.get_op(), *[__simplify_everywhere(sub) for sub in simplified.get_terms()])

def push_neg(P):
    if P.is_literal():
        return P

    if P.get_op() in ['&', '|']:
        return Prop(P.get_op(), *[push_neg(sub) for sub in P.get_terms()])
    
    if P.get_op() == '~':
        term = P.get_terms()[0]
        if term.is_atomic():
            return S

        if term.get_op() == '&':
            return Prop('|', *[push_neg(~sub) for sub in term.get_terms()])
        if term.get_op() == '|':
            return Prop('&', *[push_neg(~sub) for sub in term.get_terms()])
        if term.get_op() == '~':
            return push_neg(term.get_terms()[0])
    
    assert(False)

def propagate_hypothesis(P, domain=set()):
    positive = set()
    negative = set()
    for e in domain:
        if e == __falseProp:
            return __falseProp
        if e.is_atomic():
            positive.add(e)
        else:
            negative.add(~e)
    if len(positive & negative) > 0:
        return __falseProp
    
    if P.is_literal():
        if simplify_term(~P) in domain:
            return __falseProp
        if P in domain:
            return __trueProp
        return P

    if P.get_op() in ['~']:
        return Prop('~', propagate_hypothesis(P.get_terms()[0], domain))

    if P.get_op() in ['&', '|']:
        new_terms = []
        old_terms = []
        new_domain = set()
        for e in P.get_terms():
            if simplify_term(~e) in domain:
                new_terms.append(__falseProp)
            else:
                if e in domain:
                    new_terms.append(__trueProp)
                else:
                    if e.is_literal():
                        new_terms.append(e)
                    else:
                        old_terms.append(e)

            if e.is_literal():
                if P.get_op() == '&':
                    new_domain.add(e)
                else:
                    new_domain.add(simplify_term(~e))

        for e in domain:
            new_domain.add(e)
        
        return Prop(P.get_op(), *(new_terms + [propagate_hypothesis(sub, new_domain) for sub in old_terms]))

    assert(False)

def factor_local(P):
    if P.is_literal():
        return P

    subterms = []
    for term in P.get_terms():
        if term.is_literal():
            if term.is_atomic():
                subterms.append([{term}, {term}, term])
            else:
                subterms.append([{simplify_term(~term)}, {term}, term])
        else:
            sub_p = set()
            sub_p_raw = set()
            for sub in term.get_terms():
                if sub.is_positive():
                    sub_p.add(sub)
                    sub_p_raw.add(sub)
                else:
                    sub_p.add(simplify_term(~sub))
                    sub_p_raw.add(sub)
            subterms.append([sub_p, sub_p_raw, term])
    
    counts = {}
    counts_raw = {}
    for s, r, _ in subterms:
        for ss in s:
            if ss not in counts:
                counts[ss] = 0
            counts[ss] += 1
        for sr in r:
            if sr not in counts_raw:
                counts_raw[sr] = 0
            counts_raw[sr] += 1
    
    # MODDED: Converted the dict_keys object returned by counts.keys() to a list, for Python3.
    sp = list(counts.keys())

    sp.sort(key = lambda x: counts[x], reverse=True)
    if counts[sp[0]] <= 2:
        # MODDED: Converted the dict_keys object returned by counts_raw.keys() to a list, for Python3.
        sp = list(counts_raw.keys())
        sp.sort(key = lambda x: counts_raw[x], reverse=True)
        if counts_raw[sp[0]] < 2:
            return P
        else:
            to_keep = [x[2] for x in subterms if sp[0] not in x[1]]
            to_factor = Prop(P.get_op(), *[x[2] for x in subterms if sp[0] in x[1]])
            return Prop(P.get_op(), (sp[0] & propagate_hypothesis(to_factor, {sp[0]})) | \
                                     (~sp[0] & propagate_hypothesis(to_factor, {simplify_term(~sp[0])})), *to_keep)
    
    to_keep = [x[2] for x in subterms if sp[0] not in x[0]]
    to_factor = Prop(P.get_op(), *[x[2] for x in subterms if sp[0] in x[0]])
    return Prop(P.get_op(), (sp[0] & propagate_hypothesis(to_factor, {sp[0]})) | \
                            (~sp[0] & propagate_hypothesis(to_factor, {simplify_term(~sp[0])})), *to_keep)

def factor_at_top(P):
    if P.is_literal():
        return P
    
    factored = factor_local(P)

    if factored is not P:
        return factored
    
    return Prop(P.get_op(), *[factor_at_top(sub) for sub in P.get_terms()])

def simplify_basic(P):
    res = associative_collect(P)
    res = simplify_everywhere(res)
    res = push_neg(res)
    res = associative_collect(res)
    return simplify_everywhere(res)

def simplify_pure(P):
    """Apply various strategies until reaching fixed point."""
    res = simplify_basic(P)
    
    old = res
    res = propagate_hypothesis(res)
    while True:
        res = associative_collect(res)
        res = simplify_everywhere(res)
        if old == res:
            res = factor_at_top(res)
            if res == old:
                break
            res = simplify_everywhere(res)
            res = associative_collect(res)
        old = res
        res = propagate_hypothesis(res)

    return res

def hypo(P, X):
    return simplify_pure((X & propagate_hypothesis(P, {X}))|((~X & propagate_hypothesis(P, {~X}))))

def hypo2(P, X, Y):
    return simplify_pure((X & Y & propagate_hypothesis(P, {X, Y})) | \
                    (~X & Y & propagate_hypothesis(P, {~X, Y})) | \
                    (X & ~Y & propagate_hypothesis(P, {X, ~Y})) | \
                    (~X & ~Y & propagate_hypothesis(P, {~X, ~Y})))

def hypo3(P, X, Y, Z):
    return simplify_pure((X & Y & Z & propagate_hypothesis(P, {X, Y, Z})) | \
                    (~X & Y & Z & propagate_hypothesis(P, {~X, Y, Z})) | \
                    (X & ~Y & Z & propagate_hypothesis(P, {X, ~Y, Z})) | \
                    (~X & ~Y & Z & propagate_hypothesis(P, {~X, ~Y, Z})) | \
                    (X & Y & ~Z & propagate_hypothesis(P, {X, Y, ~Z})) | \
                    (~X & Y & ~Z & propagate_hypothesis(P, {~X, Y, ~Z})) | \
                    (X & ~Y & ~Z & propagate_hypothesis(P, {X, ~Y, ~Z})) | \
                    (~X & ~Y & ~Z & propagate_hypothesis(P, {~X, ~Y, ~Z})))

def ultra_simplify(P, depth=2):
    """P should always be in simplified form. The depth
        parameter controls the size of the hypothesis, the
        running time is directly proportional to #atoms^depth, so
        use with precaution."""
    if P.size() < 6:
        return None
    
    # MODDED: Converted the dict_keys object returned by P.var_stats().keys() to a list, for Python3.
    vars_ = list(P.var_stats().keys())
    
    #try first order
    min_size = P.size()
    for var in vars_:
        c = hypo(P, var)
        if c.size() < min_size:
            min_size = c.size()
            candidate = c
    
    if min_size < P.size():
        return candidate
    
    if depth > 1:
        #try second order
        for i in range(len(vars_)):
            for j in range(i+1, len(vars_)):
                c = hypo2(P, vars_[i], vars_[j])
                if c.size() < min_size:
                    min_size = c.size()
                    candidate = c
        
        if min_size < P.size():
            return candidate
    
    if depth > 2:
        #try third order
        for i in range(len(vars_)):
            for j in range(i+1, len(vars_)):
                for k in range(j+1, len(vars_)):
                    c = hypo3(P, vars_[i], vars_[j], vars_[k])
                    if c.size() < min_size:
                        min_size = c.size()
                        candidate = c
        
        if min_size < P.size():
            return candidate
    
    # end by simplifying down the tree
    new_terms = []
    old_terms = []
    for sub in P.get_terms():
        x = ultra_simplify(sub, depth)
        if x is not None:
            new_terms.append(x)
        else:
            old_terms.append(sub)
    
    if len(new_terms) > 0:
        return simplify_pure(Prop(P.get_op(), *(new_terms+old_terms)))
    
    return None

def simplify(P, depth=2):
    res = simplify_pure(P)
    while True:
        res_new = ultra_simplify(res)
        if res_new is None:
            return res
        res = res_new
