from ..interface import Contract, ContractNotRespected, add_prefix
from ..pyparsing_utils import myOperatorPrecedence
from ..syntax import ParsingTmp, W, opAssoc, simple_contract
from .extensions import Extension
from .suggester import create_suggester

NOT_GLYPH = '!'
AND_GLYPH = ','
OR_GLYPH = '|'

class Logical(object):
    def __init__(self, glyph, precedence):
        self.glyph = glyph
        self.precedence = precedence

    def __str__(self):
        s = self.glyph.join(self._convert(x) for x in self.clauses)
        return s

    def _convert(self, x):
        if isinstance(x, Logical) and x.precedence < self.precedence:
            return '(%s)' % x
        return '%s' % x

class OR(Logical, Contract):
    def __init__(self, clauses, where=None):
        assert isinstance(clauses, list)
        assert len(clauses) >= 2
        Contract.__init__(self, where)
        Logical.__init__(self, OR_GLYPH, 1)
        self.clauses = clauses

    def _check_quick(self, context, value):
        """ Returns True if this checks out. """

        # first make a quick pass
        for c in self.clauses:
            try:
                # try with fake context
                c._check_contract(context.copy(), value, silent=True)
                # if ok, do with main context
                c._check_contract(context, value, silent=True)
                return True
            except ContractNotRespected as e:
                pass
        
        return False


    def check_contract(self, context, value, silent):
        orig = context.copy()

        if self._check_quick(context, value):
            return
        else:
            if silent:
                msg = '(Error description suppressed.)'
                raise ContractNotRespected(contract=self, error=msg,
                        value=value, context=context)

            # otherwise need to do it again with detailed error messages
            self.get_error(orig, value)

    def get_error(self, context, value):
        """ This assumes that we are going to fail """
        exceptions = []
        for c in self.clauses:
            try:
                # try with fake context
                c._check_contract(context.copy(), value, silent=False)
                # if ok, do with main context
                c._check_contract(context, value, silent=False)

                assert False, "We should not be here."
            except ContractNotRespected as e:
                exceptions.append((c, e))
        else:
            msg = self._format_exceptions(exceptions)
            raise ContractNotRespected(contract=self, error=msg,
                        value=value, context=context)

    def _format_exceptions(self, exceptions):
        msg = ('Could not satisfy any of the %d clauses in %s.'
               % (len(self.clauses), self))

        for i, ex in enumerate(exceptions):
            c, e = ex
            if i + 1 > 1:
                msg += '\n ├┄┄┄ Clause #%d:   %s\n' % (i + 1, c)
            else:
                msg += '\n ┬┄┄┄┄ Clause #%d:   %s\n' % (i + 1, c)
            msg += add_prefix('%s' % e, ' | ')

        msg += '\n ╰┄┄┄┄┄┄ (end clauses) ┄┄┄┄┄┄'
        return msg

    def __repr__(self):
        s = 'OR(%r)' % self.clauses
        return s

    @staticmethod
    def parse_action(string, location, tokens):
        l = list(tokens[0])
        clauses = [l.pop(0)]
        while l:
            glyph = l.pop(0)  # @UnusedVariable
            assert glyph == OR_GLYPH
            operand = l.pop(0)
            clauses.append(operand)
        where = W(string, location)
        return OR(clauses, where=where)


class And(Logical, Contract):
    def __init__(self, clauses, where=None):
        assert isinstance(clauses, list)
        assert len(clauses) >= 2, clauses
        Contract.__init__(self, where)
        Logical.__init__(self, AND_GLYPH, 2)
        self.clauses = clauses

    def check_contract(self, context, value, silent):
        for c in self.clauses:
            c._check_contract(context, value, silent)

    def __repr__(self):
        s = 'And(%r)' % self.clauses
        return s

    @staticmethod
    def parse_action(string, location, tokens):
        l = list(tokens[0])
        clauses = [l.pop(0)]
        while l:
            glyph = l.pop(0)  # @UnusedVariable
            assert glyph == AND_GLYPH
            operand = l.pop(0)
            clauses.append(operand)
        where = W(string, location)
        return And(clauses, where=where)


class Not(Logical, Contract):
    def __init__(self, clauses, where=None):
        assert isinstance(clauses, list)
        assert len(clauses) == 1, clauses
        Contract.__init__(self, where)
        Logical.__init__(self, NOT_GLYPH, 3)
        self.clauses = clauses

    def check_contract(self, context, value, silent):
        clause = self.clauses[0]
        try:
            clause._check_contract(context, value, silent)
        except ContractNotRespected:
            pass
        else:
            msg = "Shouldn't have satisfied the clause %s." % clause
            raise ContractNotRespected(contract=self, error=msg,
                                       value=value, context=context)

    @staticmethod
    def parse_action(string, location, tokens):
        l = list(tokens[0])
        assert l.pop(0) == NOT_GLYPH
        where = W(string, location)
        return Not(l, where=where)

    def __repr__(self):
        s = 'Not(%r)' % self.clauses
        return s

    def __str__(self):
        return self.glyph + self._convert(self.clauses[0])


suggester = create_suggester(get_options=lambda: ParsingTmp.keywords +
                             list(Extension.registrar.keys()))
baseExpr = simple_contract | suggester
baseExpr.setName('Simple contract (recovering)')


op = myOperatorPrecedence
# op = operatorPrecedence
composite_contract = op(baseExpr, [
                         (NOT_GLYPH, 1, opAssoc.RIGHT, Not.parse_action),
                         (AND_GLYPH, 2, opAssoc.LEFT, And.parse_action),
                         (OR_GLYPH, 2, opAssoc.LEFT, OR.parse_action),
                    ])
composite_contract.setName('NOT/OR/AND contract')

or_contract = op(baseExpr, [
                         (OR_GLYPH, 2, opAssoc.LEFT, OR.parse_action),
                    ])
or_contract.setName('OR contract')
