# Copyright 2019 Ingmar Dasseville, Pierre Carbonnelle
#
# This file is part of Interactive_Consultant.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

"""

Methods to simplify a logic expression.

This module monkey-patches the Expression class and sub-classes.


"""

import sys
from typing import List

from .Expression import (
    Constructor, Expression, AIfExpr, AQuantification, Quantee, Subtype,
    Operator, AEquivalence, AImplication, ADisjunction,
    AConjunction, AComparison, ASumMinus, AMultDiv, APower,
    AUnary, AAggregate, SymbolExpr, AppliedSymbol, UnappliedSymbol, Variable,
    Number, Date, Brackets, TRUE, FALSE, NOT, AND, OR)
from .Parse import Symbol, Enumeration, Tuple, TypeDeclaration
from .Assignments import Status as S, Assignment
from .utils import BOOL, INT, CONCEPT, ABS, RESERVED_SYMBOLS


# class Expression  ###########################################################

def _change(self, sub_exprs=None, ops=None, value=None, simpler=None,
            co_constraint=None):
    " change attributes of an expression, and resets derived attributes "

    if sub_exprs is not None:
        self.sub_exprs = sub_exprs
    if ops is not None:
        self.operator = ops
    if co_constraint is not None:
        self.co_constraint = co_constraint
    if value is not None:
        self.value = value
    elif simpler is not None:
        if simpler.value is not None:  # example: prime.idp
            self.value = simpler.value
        else:
            self.simpler = simpler
    assert (self.value is None
               or type(self.value) in [AppliedSymbol, UnappliedSymbol, Symbol,
                                       Number, Date, Subtype]), \
            f"Incorrect value in _change: {self.value}"

    # reset derived attributes
    self.str = sys.intern(str(self))

    return self
Expression._change = _change


def update_exprs(self, new_exprs):
    """ change sub_exprs and simplify, while keeping relevant info. """
    #  default implementation, without simplification
    return self._change(sub_exprs=list(new_exprs))
Expression.update_exprs = update_exprs


def simplify1(self):
    return self.update_exprs(self.sub_exprs)
Expression.simplify1 = simplify1



# Class AIfExpr  ###############################################################


def update_exprs(self, new_exprs):
    sub_exprs = list(new_exprs)
    if_, then_, else_ = sub_exprs[0], sub_exprs[1], sub_exprs[2]
    if if_.same_as(TRUE):
        return self._change(simpler=then_, sub_exprs=sub_exprs)
    elif if_.same_as(FALSE):
        return self._change(simpler=else_, sub_exprs=sub_exprs)
    else:
        if then_.same_as(else_):
            return self._change(simpler=then_, sub_exprs=sub_exprs)
        elif then_.same_as(TRUE) and else_.same_as(FALSE):
            return self._change(simpler=if_, sub_exprs=sub_exprs)
        elif then_.same_as(FALSE) and else_.same_as(TRUE):
            return self._change(simpler=NOT(if_),
                                sub_exprs=sub_exprs)
    return self._change(sub_exprs=sub_exprs)
AIfExpr.update_exprs = update_exprs


# Class Quantee  #######################################################

def update_exprs(self, new_exprs):
    if not self.decl and self.sub_exprs:
        symbol = self.sub_exprs[0].value
        if symbol:
            self.decl = symbol.decl
            self.sub_exprs[0].decl = self.decl
    return self
Quantee.update_exprs = update_exprs


# Class AQuantification  ######################################################

def update_exprs(self, new_exprs):
    exprs = list(new_exprs)
    if not self.quantees:
        if self.q == '∀':
            simpler = AND(exprs)
        else:
            simpler = OR(exprs)
        return self._change(simpler=simpler, sub_exprs=[simpler])
    return self._change(sub_exprs=exprs)
AQuantification.update_exprs = update_exprs


# Class AImplication  #######################################################

def update_exprs(self, new_exprs):
    if type(new_exprs) == list:
        new_exprs = iter(new_exprs)
    exprs0 = next(new_exprs)
    value, simpler = None, None
    if exprs0.same_as(FALSE):  # (false => p) is true
        # exprs[0] may be false because exprs[1] was false
        exprs1 = self.sub_exprs[1] if self.sub_exprs[1].same_as(FALSE)\
            else FALSE
        value = TRUE
    elif exprs0.same_as(TRUE):  # (true => p) is p
        exprs1 = next(new_exprs)
        simpler = exprs1
    else:
        exprs1 = next(new_exprs)
        if exprs1.same_as(TRUE):  # (p => true) is true
            exprs0 = exprs0 if self.sub_exprs[0].same_as(TRUE) else TRUE
            value = TRUE
        elif exprs1.same_as(FALSE):  # (p => false) is ~p
            simpler = NOT(exprs0)
    return self._change(value=value, simpler=simpler,
                        sub_exprs=[exprs0, exprs1])
AImplication.update_exprs = update_exprs


# Class AEquivalence  #######################################################

def update_exprs(self, new_exprs):
    exprs = list(new_exprs)
    if len(exprs) == 1:
        return self._change(simpler=exprs[1], sub_exprs=exprs)
    for e in exprs:
        if e.same_as(TRUE):  # they must all be true
            return self._change(simpler=AND(exprs),
                                sub_exprs=exprs)
        if e.same_as(FALSE):  # they must all be false
            return self._change(simpler=AND([NOT(e) for e in exprs]),
                                sub_exprs=exprs)
    return self._change(sub_exprs=exprs)
AEquivalence.update_exprs = update_exprs


# Class ADisjunction  #######################################################

def update_exprs(self, new_exprs):
    exprs, other = [], []
    value, simpler = None, None
    for i, expr in enumerate(new_exprs):
        if expr.same_as(TRUE):
            # simplify only if one other sub_exprs was unknown
            if any(e.value is None and not i == j for j, e in enumerate(self.sub_exprs)):
                return self._change(value=TRUE, sub_exprs=[expr])
            value = TRUE
        exprs.append(expr)
        if not expr.same_as(FALSE):
            other.append(expr)

    if len(other) == 0:  # all disjuncts are False
        value = FALSE
    if len(other) == 1:
        simpler = other[0]
    return self._change(value=value, simpler=simpler, sub_exprs=exprs)
ADisjunction.update_exprs = update_exprs


# Class AConjunction  #######################################################

# same as ADisjunction, with TRUE and FALSE swapped
def update_exprs(self, new_exprs):
    exprs, other = [], []
    value, simpler = None, None
    for i, expr in enumerate(new_exprs):
        if expr.same_as(FALSE):
            # simplify only if one other sub_exprs was unknown
            if any(e.value is None and not i == j for j, e in enumerate(self.sub_exprs)):
                return self._change(value=FALSE, sub_exprs=[expr])
            value = FALSE
        exprs.append(expr)
        if not expr.same_as(TRUE):
            other.append(expr)

    if len(other) == 0:  # all conjuncts are True
        value = TRUE
    if len(other) == 1:
        simpler = other[0]
    return self._change(value=value, simpler=simpler, sub_exprs=exprs)
AConjunction.update_exprs = update_exprs


# Class AComparison  #######################################################

def update_exprs(self, new_exprs):
    operands = list(new_exprs)
    operands1 = [e.value for e in operands]
    if all(e is not None for e in operands1):
        acc, acc1 = operands[0], operands1[0]
        assert len(self.operator) == len(operands1[1:]), "Internal error"
        for op, expr, expr1 in zip(self.operator, operands[1:], operands1[1:]):
            if op == "=":
                if not acc1.same_as(expr1):
                    return self._change(value=FALSE, sub_exprs=[acc, expr], ops=[op])
            elif not (Operator.MAP[op]) (acc1.py_value, expr1.py_value):
                return self._change(value=FALSE, sub_exprs=[acc, expr], ops=[op])
            acc, acc1 = expr, expr1
        return self._change(value=TRUE, sub_exprs=operands)
    return self._change(sub_exprs=operands)
AComparison.update_exprs = update_exprs

def as_set_condition(self):
    return ((None, None, None) if not self.is_assignment() else
            (self.sub_exprs[0], True,
             Enumeration(tuples=[Tuple(args=[self.sub_exprs[1]])])))
AComparison.as_set_condition = as_set_condition

#############################################################

def update_arith(self, family, operands):
    operands = list(operands)
    operands1 = [e.value for e in operands]
    if all(e is not None for e in operands1):
        self.check(all(hasattr(e, 'py_value') for e in operands1),
                f"Incorrect numeric type in {self}")
        out = operands1[0].py_value

        assert len(self.operator) == len(operands1[1:]), "Internal error"
        for op, e in zip(self.operator, operands1[1:]):
            function = Operator.MAP[op]

            if op == '/' and self.type == INT:  # integer division
                out //= e.py_value
            else:
                out = function(out, e.py_value)
        value = Number(number=str(out))
        return self._change(value=value, sub_exprs=operands)
    return self._change(sub_exprs=operands)


# Class ASumMinus  #######################################################

def update_exprs(self, new_exprs):
    return update_arith(self, '+', new_exprs)
ASumMinus.update_exprs = update_exprs


# Class AMultDiv  #######################################################

def update_exprs(self, new_exprs):
    operands = list(new_exprs)
    if any(op == '%' for op in self.operator):  # special case !
        operands1 = [e.value for e in operands]
        if len(operands) == 2 \
           and all(e is not None for e in operands1):
            out = operands1[0].py_value % operands1[1].py_value
            return self._change(value=Number(number=str(out)), sub_exprs=operands)
        else:
            return self._change(sub_exprs=operands)
    return update_arith(self, '⨯', operands)
AMultDiv.update_exprs = update_exprs


# Class APower  #######################################################

def update_exprs(self, new_exprs):
    operands = list(new_exprs)
    operands1 = [e.value for e in operands]
    if len(operands) == 2 \
       and all(e is not None for e in operands1):
        out = operands1[0].py_value ** operands1[1].py_value
        return self._change(value=Number(number=str(out)), sub_exprs=operands)
    else:
        return self._change(sub_exprs=operands)
APower.update_exprs = update_exprs


# Class AUnary  #######################################################

def update_exprs(self, new_exprs):
    operand = list(new_exprs)[0]
    if self.operator == '¬':
        if operand.same_as(TRUE):
            return self._change(value=FALSE, sub_exprs=[operand])
        if operand.same_as(FALSE):
            return self._change(value=TRUE, sub_exprs=[operand])
    else:  # '-'
        a = operand.value
        if a is not None:
            if type(a) == Number:
                return self._change(value=Number(number=f"{-a.py_value}"),
                                    sub_exprs=[operand])
    return self._change(sub_exprs=[operand])
AUnary.update_exprs = update_exprs

def as_set_condition(self):
    (x, y, z) = self.sub_exprs[0].as_set_condition()
    return ((None, None, None) if x is None else
            (x, not y, z))
AUnary.as_set_condition = as_set_condition


# Class AAggregate  #######################################################

def update_exprs(self, new_exprs):
    operands = list(new_exprs)
    if self.annotated and not self.quantees:
        operands1 = [e.value for e in operands]
        if all(e is not None for e in operands1):
            out = sum(e.py_value for e in operands1)
            out = Number(number=str(out))
            return self._change(value=out, sub_exprs=operands)
    return self._change(sub_exprs=operands)
AAggregate.update_exprs = update_exprs


# Class AppliedSymbol  #######################################################

def update_exprs(self, new_exprs):
    new_exprs = list(new_exprs)
    if not self.decl:
        symbol = self.symbol.value
        if symbol:
            self.decl = symbol.decl
    self.type = (BOOL if self.is_enumerated or self.in_enumeration else
            self.decl.type if self.decl else None)
    if self.decl and type(self.decl) == Constructor:
        if all(e.value is not None for e in new_exprs):
            return self._change(sub_exprs=new_exprs, value = self)

    # simplify abs()
    if (self.decl and self.decl.name == ABS and len(new_exprs) == 1
        and new_exprs[0].value):
        value = Number(number=str(abs(new_exprs[0].py_value)))
        return self._change(value=value, sub_exprs=new_exprs)

    # simplify x(pos(0,0)) to 0,  is_pos(pos(0,0)) to True
    if (len(new_exprs) == 1
        and hasattr(new_exprs[0], 'decl')
        and type(new_exprs[0].decl) == Constructor
        and new_exprs[0].decl.tester
        and self.decl):
        if self.decl.name in new_exprs[0].decl.parent.accessors:
            i = new_exprs[0].decl.parent.accessors[self.decl.name]
            return self._change(simpler=new_exprs[0].sub_exprs[i], sub_exprs=new_exprs)
        if self.decl.name == new_exprs[0].decl.tester.name:
            return self._change(value=TRUE, sub_exprs=new_exprs)

    return self._change(sub_exprs=new_exprs)
AppliedSymbol.update_exprs = update_exprs

def as_set_condition(self):
    # determine core after substitutions
    core = AppliedSymbol.make(self.symbol, self.sub_exprs).copy()

    return ((None, None, None) if not self.in_enumeration else
            (core, 'not' not in self.is_enumeration, self.in_enumeration))
AppliedSymbol.as_set_condition = as_set_condition


# Class SymbolExpr  #######################################################

def update_exprs(self, new_exprs):
    symbol = list(new_exprs)[0]
    value = (symbol if self.eval == '' else
             symbol.decl.symbol if type(symbol) == UnappliedSymbol and symbol.decl else
             None)
    self.check(not value or type(value) != Variable,
               f"Variable `{value}` cannot be applied to argument(s).")
    self.decl = value.decl if value else None
    return self._change(sub_exprs=[symbol], value=value)
SymbolExpr.update_exprs = update_exprs


# Class Brackets  #######################################################

def update_exprs(self, new_exprs):
    expr = list(new_exprs)[0]
    return self._change(sub_exprs=[expr], value=expr.value)
Brackets.update_exprs = update_exprs


# set conditions  #######################################################

def join_set_conditions(assignments: List[Assignment]) -> List[Assignment]:
    """In a list of assignments, merge assignments that are set-conditions on the same term.

    An equality and a membership predicate (`in` operator) are both set-conditions.

    Args:
        assignments (List[Assignment]): the list of assignments to make more compact

    Returns:
        List[Assignment]: the compacted list of assignments
    """
    #
    for i, c in enumerate(assignments):
        (x, belongs, y) = c.as_set_condition()
        if x:
            for j in range(i):
                (x1, belongs1, y1) = assignments[j].as_set_condition()
                if x1 and x.same_as(x1):
                    if belongs and belongs1:
                        new_tuples = (y.tuples & y1.tuples) # intersect
                    elif belongs and not belongs1:
                        new_tuples = (y.tuples ^ y1.tuples) # difference
                    elif not belongs and belongs1:
                        belongs = belongs1
                        new_tuples = (y1.tuples ^ y.tuples)
                    else:
                        new_tuples = y.tuples | y1.tuples # union
                    # sort again
                    new_tuples = list(new_tuples.values())

                    out = AppliedSymbol.make(
                        symbol=x.symbol, args=x.sub_exprs,
                        is_enumeration='in',
                        in_enumeration=Enumeration(tuples=new_tuples)
                    )

                    core = AppliedSymbol.make(out.symbol, out.sub_exprs).copy()
                    out.simpler = out.in_enumeration.contains([core], False)

                    out = Assignment(out,
                                     TRUE if belongs else FALSE,
                                     S.UNKNOWN)

                    assignments[j] = out # keep the first one
                    assignments[i] = Assignment(TRUE, TRUE, S.UNKNOWN)
    return [c for c in assignments if c.sentence != TRUE]

Done = True
