import sys
import z3
import ctypes
import logging
import numbers
import operator
import threading
import weakref
#from past.builtins import long
from functools import reduce
from decimal import Decimal

from ..utils.transition import raise_from
from ..errors import ClaripyZ3Error

l = logging.getLogger("claripy.backends.backend_z3")

#pylint:disable=unidiomatic-typecheck

try:
    import __pypy__
    _is_pypy = True
except ImportError:
    _is_pypy = False


def _add_memory_pressure(p):
    """
    PyPy's garbage collector is not aware of memory uses happening inside native code. When performing memory-intensive
    tasks in native code, the memory pressure that PyPy observes can greatly deviate from the actual memory pressure.
    We must manually add sufficient memory pressure to account for the "missing" portion.

    This is not a problem for CPython since its GC is based on reference counting.
    """

    global _is_pypy
    if _is_pypy:
        __pypy__.add_memory_pressure(p)


#
# Some global variables
#

# track the count of solves
solve_count = 0

supports_fp = hasattr(z3, 'fpEQ')

#
# Utility functions
#

def condom(f):
    def z3_condom(*args, **kwargs):
        """
        The Z3 condom intercepts Z3Exceptions and throws a ClaripyZ3Error instead.
        """
        try:
            if int is not long:
                condom_args = tuple((int(a) if type(a) is long and a < sys.maxint else a) for a in args)
            else:
                condom_args = args
            return f(*condom_args, **kwargs)
        except z3.Z3Exception as ze:
            raise_from(ClaripyZ3Error("Z3Exception: %s" % ze), ze)
    return z3_condom

def _raw_caller(f):
    @staticmethod
    @condom
    def raw_caller(*args, **kwargs):
        return f(*args, **kwargs)
    return raw_caller

#
# And the (ugh) magic
#

from . import Backend
class BackendZ3(Backend):
    _split_on = { 'And', 'Or' }

    def __init__(self):
        Backend.__init__(self, solver_required=True)
        self._enable_simplification_cache = False
        self._hash_to_constraint = weakref.WeakValueDictionary()

        # and the operations
        all_ops = backend_fp_operations | backend_operations if supports_fp else backend_operations
        for o in all_ops - {'BVV', 'BoolV', 'FPV', 'FPS', 'BitVec'}:
            self._op_raw[o] = getattr(self, '_op_raw_' + o)
        self._op_raw['Xor'] = self._op_raw_Xor

        self._op_raw['__ge__'] = self._op_raw_UGE
        self._op_raw['__gt__'] = self._op_raw_UGT
        self._op_raw['__le__'] = self._op_raw_ULE
        self._op_raw['__lt__'] = self._op_raw_ULT

        self._op_raw['Reverse'] = self._op_raw_Reverse
        self._op_raw['Identical'] = self._identical
        self._op_raw['fpToSBV'] = self._op_raw_fpToSBV
        self._op_raw['fpToUBV'] = self._op_raw_fpToUBV

        self._op_expr['BVS'] = self.BVS
        self._op_expr['BVV'] = self.BVV
        self._op_expr['FPV'] = self.FPV
        self._op_expr['FPS'] = self.FPS
        self._op_expr['BoolV'] = self.BoolV
        self._op_expr['BoolS'] = self.BoolS

        self._op_raw['__floordiv__'] = self._op_div
        self._op_raw['__mod__'] = self._op_mod

        # reduceable
        self._op_raw['__add__'] = self._op_add
        self._op_raw['__sub__'] = self._op_sub
        self._op_raw['__mul__'] = self._op_mul
        self._op_raw['__or__'] = self._op_or
        self._op_raw['__xor__'] = self._op_xor
        self._op_raw['__and__'] = self._op_and

        # XXX this is a HUGE HACK that should be removed whenever uninitialized gets moved to the
        # "proposed annotation backend" or wherever will prevent it from being part of the object
        # identity. also whenever the VSA attributes get the fuck out of BVS as well
        self._tls.extra_bvs_data = {}


    @property
    def _c_uint64_p(self):
        try:
            return self._tls.c_uint64_p
        except AttributeError:
            # a pointer to get values out of Z3
            self._tls.c_uint64_p = ctypes.pointer(ctypes.c_uint64())

            return self._tls.c_uint64_p

    @property
    def _context(self):
        try:
            return self._tls.context
        except AttributeError:
            self._tls.context = z3.Context() if threading.current_thread().name != 'MainThread' else z3.main_ctx()
            return self._tls.context

    @property
    def _boolref_tactics(self):
        try:
            return self._tls.boolref_tactics
        except AttributeError:
            tactics = z3.Then(
                z3.Tactic("simplify", ctx=self._context),
                z3.Tactic("propagate-ineqs", ctx=self._context),
                z3.Tactic("propagate-values", ctx=self._context),
                z3.Tactic("unit-subsume-simplify", ctx=self._context),
                z3.Tactic("aig", ctx=self._context),
                ctx=self._context
            )
            self._tls.boolref_tactics = tactics
            return self._tls.boolref_tactics

    @property
    def _ast_cache(self):
        try:
            return self._tls.ast_cache
        except AttributeError:
            self._tls.ast_cache = weakref.WeakValueDictionary()
            return self._tls.ast_cache

    @property
    def _var_cache(self):
        try:
            return self._tls.var_cache
        except AttributeError:
            self._tls.var_cache = weakref.WeakValueDictionary()
            return self._tls.var_cache

    @property
    def _sym_cache(self):
        try:
            return self._tls.sym_cache
        except AttributeError:
            self._tls.sym_cache = weakref.WeakValueDictionary()
            return self._tls.sym_cache

    @property
    def _simplification_cache_key(self):
        try:
            return self._tls.simplification_cache_key
        except AttributeError:
            self._tls.simplification_cache_key = weakref.WeakValueDictionary()
            return self._tls.simplification_cache_key

    @property
    def _simplification_cache_val(self):
        try:
            return self._tls.simplification_cache_val
        except AttributeError:
            self._tls.simplification_cache_val = weakref.WeakValueDictionary()
            return self._tls.simplification_cache_val

    def downsize(self):
        Backend.downsize(self)

        self._ast_cache.clear()
        self._var_cache.clear()
        self._sym_cache.clear()
        self._simplification_cache_key.clear()
        self._simplification_cache_val.clear()

    @condom
    def _size(self, e):
        if not isinstance(e, z3.BitVecRef) and not isinstance(e, z3.BitVecNumRef):
            l.debug("Unable to determine length of value of type %s", e.__class__)
            raise BackendError("Unable to determine length of value of type %s" % e.__class__)
        return e.size()

    def _name(self, e): #pylint:disable=unused-argument
        l.warning("BackendZ3.name() called. This is weird.")
        raise BackendError("name is not implemented yet")

    #
    # Core creation methods
    #

    @condom
    def BVS(self, ast):
        name, mn, mx, stride, uninit, discrete, discrete_max = ast.args
        self._tls.extra_bvs_data[name] = (mn, mx, stride, uninit, discrete, discrete_max)
        size = ast.size()
        expr = z3.BitVec(name, size, ctx=self._context)
        #if mn is not None:
        #    expr = z3.If(z3.ULT(expr, mn), mn, expr, ctx=self._context)
        #if mx is not None:
        #    expr = z3.If(z3.UGT(expr, mx), mx, expr, ctx=self._context)
        #if stride is not None:
        #    expr = (expr / stride) * stride
        return expr

    @condom
    def BVV(self, ast): #pylint:disable=unused-argument
        if ast.args[0] is None:
            raise BackendError("Z3 can't handle empty BVVs")

        size = ast.size()
        return z3.BitVecVal(ast.args[0], size, ctx=self._context)

    @condom
    def FPS(self, ast): #pylint:disable=unused-argument
        name, sort_claripy = ast.args
        sort_z3 = self._convert(sort_claripy)
        return z3.FP(name, sort_z3, ctx=self._context)

    @condom
    def FPV(self, ast): #pylint:disable=unused-argument
        val = str(ast.args[0])
        sort = self._convert(ast.args[1])
        if val == 'inf':
            return z3.fpPlusInfinity(sort)
        elif val == '-inf':
            return z3.fpMinusInfinity(sort)
        elif val == '0.0':
            return z3.fpPlusZero(sort)
        elif val == '-0.0':
            return z3.fpMinusZero(sort)
        elif val == 'nan':
            return z3.fpNaN(sort)
        else:
            better_val = str(Decimal(ast.args[0]))
            return z3.FPVal(better_val, sort, ctx=self._context)

    @condom
    def BoolS(self, ast): #pylint:disable=unused-argument
        return z3.Bool(ast.args[0], ctx=self._context)

    @condom
    def BoolV(self, ast): #pylint:disable=unused-argument
        return z3.BoolVal(ast.args[0], ctx=self._context)

    #
    # Conversions
    #

    @condom
    def _convert(self, obj):
        if isinstance(obj, FSort):
            return z3.FPSort(obj.exp, obj.mantissa, ctx=self._context)
        elif isinstance(obj, RM):
            if obj == RM_RNE:
                return z3.RNE(ctx=self._context)
            elif obj == RM_RNA:
                return z3.RNA(ctx=self._context)
            elif obj == RM_RTP:
                return z3.RTP(ctx=self._context)
            elif obj == RM_RTN:
                return z3.RTN(ctx=self._context)
            elif obj == RM_RTZ:
                return z3.RTZ(ctx=self._context)
            else:
                raise BackendError("unrecognized rounding mode")
        elif obj is True:
            return z3.BoolVal(True, ctx=self._context)
        elif obj is False:
            return z3.BoolVal(False, ctx=self._context)
        elif isinstance(obj, (numbers.Number, str)):
            return obj
        elif hasattr(obj, '__module__') and obj.__module__ in ('z3', 'z3.z3'):
            return obj
        else:
            l.debug("BackendZ3 encountered unexpected type %s", type(obj))
            raise BackendError("unexpected type %s encountered in BackendZ3" % type(obj))

    def call(self, *args, **kwargs):
        return Backend.call(self, *args, **kwargs)

    @condom
    def _abstract(self, z):
        #return self._abstract(z, split_on=split_on)[0]
        return self._abstract_internal(z.ctx.ctx, z.ast)

    @staticmethod
    def _z3_ast_hash(ctx, ast):
        """
        This is a better hashing function for z3 Ast objects. Z3_get_ast_hash() creates too many hash collisions.

        :param ctx: A z3 Context.
        :param ast: A z3 Ast object.
        :return:    An integer - the hash.
        """

        z3_hash = z3.Z3_get_ast_hash(ctx, ast)
        z3_ast_ref = ast.value # this seems to be the memory address
        z3_sort = z3.Z3_get_sort(ctx, ast).value
        return "%d_%d_%d" % (z3_hash, z3_sort, z3_ast_ref)

    def _abstract_internal(self, ctx, ast, split_on=None):
        h = self._z3_ast_hash(ctx, ast)
        try:
            return self._ast_cache[h]
        except KeyError:
            pass

        decl = z3.Z3_get_app_decl(ctx, ast)
        decl_num = z3.Z3_get_decl_kind(ctx, decl)
        z3_sort = z3.Z3_get_sort(ctx, ast)

        if decl_num not in z3_op_nums:
            raise ClaripyError("unknown decl kind %d" % decl_num)
        if z3_op_nums[decl_num] not in op_map:
            raise ClaripyError("unknown decl op %s" % z3_op_nums[decl_num])
        op_name = op_map[z3_op_nums[decl_num]]

        num_args = z3.Z3_get_app_num_args(ctx, ast)
        split_on = self._split_on if split_on is None else split_on
        new_split_on = split_on if op_name in split_on else set()
        children = [ self._abstract_internal(ctx, z3.Z3_get_app_arg(ctx, ast, i), new_split_on) for i in range(num_args) ]

        append_children = True

        if op_name == 'True':
            return BoolV(True)
        elif op_name == 'False':
            return BoolV(False)
        elif op_name.startswith('RM_'):
            return RM.from_name(op_name)
        elif op_name == 'BitVecVal':
            bv_size = z3.Z3_get_bv_sort_size(ctx, z3_sort)
            if z3.Z3_get_numeral_uint64(ctx, ast, self._c_uint64_p):
                return BVV(self._c_uint64_p.contents.value, bv_size)
            else:
                bv_num = int(z3.Z3_get_numeral_string(ctx, ast))
                return BVV(bv_num, bv_size)
        elif op_name in ('FPVal', 'MinusZero', 'MinusInf', 'PlusZero', 'PlusInf', 'NaN'):
            ebits = z3.Z3_fpa_get_ebits(ctx, z3_sort)
            sbits = z3.Z3_fpa_get_sbits(ctx, z3_sort)
            sort = FSort.from_params(ebits, sbits)
            val = self._abstract_fp_val(ctx, ast, op_name)
            return FPV(val, sort)

        elif op_name == 'UNINTERPRETED' and num_args == 0: # symbolic value
            symbol_name = z3.Z3_get_symbol_string(ctx, z3.Z3_get_decl_name(ctx, decl))
            symbol_ty = z3.Z3_get_sort_kind(ctx, z3_sort)

            if symbol_ty == z3.Z3_BV_SORT:
                bv_size = z3.Z3_get_bv_sort_size(ctx, z3_sort)
                extra_args = self._tls.extra_bvs_data.get(symbol_name, (None, None, None, False, False, None))
                return BV('BVS',
                        (symbol_name,) + extra_args,
                        length=bv_size,
                        variables={ symbol_name },
                        symbolic=True)
            elif symbol_ty == z3.Z3_BOOL_SORT:
                return Bool('BoolS',
                        (symbol_name,),
                        variables={ symbol_name },
                        symbolic=True)
            elif symbol_ty == z3.Z3_FLOATING_POINT_SORT:
                ebits = z3.Z3_fpa_get_ebits(ctx, z3_sort)
                sbits = z3.Z3_fpa_get_sbits(ctx, z3_sort)
                sort = FSort.from_params(ebits, sbits)
                return FP('FPS',
                        (symbol_name, sort),
                        variables={ symbol_name },
                        symbolic=True,
                        length=sort.length)
            else:
                raise BackendError("Unknown z3 term type %d...?" % symbol_ty)

        elif op_name == 'UNINTERPRETED':
            mystery_name = z3.Z3_get_symbol_string(ctx, z3.Z3_get_decl_name(ctx, decl))
            args = [ ]

            #
            # TODO: DEPRECATED: remove the following after some reasonable amount of time.
            #

            if mystery_name == 'bvsmod_i':
                l.error("Your Z3 is out of date. Please update angr-only-z3-custom or future releases of claripy will fail.")
                op_name = '__mod__'
                decl_num = z3.Z3_OP_BSMOD
            elif mystery_name == 'bvsdiv_i':
                l.error("Your Z3 is out of date. Please update angr-only-z3-custom or future releases of claripy will fail.")
                op_name = 'SDiv'
                decl_num = z3.Z3_OP_BSDIV
            else:
                l.error("Mystery operation %s in BackendZ3._abstract_internal. Please report this.", mystery_name)
        elif op_name == 'Extract':
            hi = z3.Z3_get_decl_int_parameter(ctx, decl, 0)
            lo = z3.Z3_get_decl_int_parameter(ctx, decl, 1)
            args = [ hi, lo ]
        elif op_name in ('SignExt', 'ZeroExt'):
            num = z3.Z3_get_decl_int_parameter(ctx, decl, 0)
            args = [ num ]
        elif op_name in ('fpToFP', 'fpToFPSigned'):
            exp = z3.Z3_fpa_get_ebits(ctx, z3_sort)
            mantissa = z3.Z3_fpa_get_sbits(ctx, z3_sort)
            sort = FSort.from_params(exp, mantissa)
            args = children + [sort]
            append_children = False
        elif op_name in ('fpToSBV', 'fpToUBV'):
            # uuuuuugggggghhhhhh
            bv_size = z3.Z3_get_bv_sort_size(ctx, z3_sort)
            args = children + [bv_size]
            append_children = False
        else:
            args = [ ]

        if append_children:
            args.extend(children)

        # hmm.... honestly not sure what to do here
        result_ty = op_type_map[z3_op_nums[decl_num]]
        ty = type(args[-1])

        if type(result_ty) is str:
            err = "Unknown Z3 error in abstraction (result_ty == '%s'). Update your version of Z3, and, if the problem persists, open a claripy issue." % result_ty
            l.error(err)
            raise BackendError(err)

        if op_name == 'If':
            # If is polymorphic and thus must be handled specially
            ty = type(args[1])

            a = ty('If', tuple(args), length=args[1].length)
        elif hasattr(ty, op_name) or hasattr(_all_operations, op_name):
            op = getattr(ty if hasattr(ty, op_name) else _all_operations, op_name)
            if op.calc_length is not None:
                length = op.calc_length(*args)
                a = result_ty(op_name, tuple(args), length=length)
            else:
                a = result_ty(op_name, tuple(args))
        else:
            a = result_ty(op_name, tuple(args))

        self._ast_cache[h] = a
        return a

    def _abstract_to_primitive(self, ctx, ast):
        decl = z3.Z3_get_app_decl(ctx, ast)
        decl_num = z3.Z3_get_decl_kind(ctx, decl)

        if decl_num not in z3_op_nums:
            raise ClaripyError("unknown decl kind %d" % decl_num)
        if z3_op_nums[decl_num] not in op_map:
            raise ClaripyError("unknown decl op %s" % z3_op_nums[decl_num])
        op_name = op_map[z3_op_nums[decl_num]]

        if op_name == 'BitVecVal':
            if z3.Z3_get_numeral_uint64(ctx, ast, self._c_uint64_p):
                return self._c_uint64_p.contents.value
            else:
                bv_num = int(z3.Z3_get_numeral_string(ctx, ast))
                return bv_num
        elif op_name == 'True':
            return True
        elif op_name == 'False':
            return False
        elif op_name in ('FPVal', 'MinusZero', 'MinusInf', 'PlusZero', 'PlusInf', 'NaN'):
            return self._abstract_fp_val(ctx, ast, op_name)
        else:
            raise BackendError("Unable to abstract Z3 object to primitive")

    def _abstract_fp_val(self, ctx, ast, op_name):
        if op_name == 'FPVal':
            # TODO: do better than this
            fp_mantissa = float(z3.Z3_fpa_get_numeral_significand_string(ctx, ast))
            fp_exp = int(z3.Z3_fpa_get_numeral_exponent_string(ctx, ast, False))
            fp_sign_c = ctypes.c_int()
            z3.Z3_fpa_get_numeral_sign(ctx, ast, ctypes.byref(fp_sign_c))
            fp_sign = -1 if fp_sign_c.value != 0 else 1
            value = fp_sign * fp_mantissa * (2 ** fp_exp)
            return value
        elif op_name == 'MinusZero':
            return -0.0
        elif op_name == 'MinusInf':
            return float('-inf')
        elif op_name == 'PlusZero':
            return 0.0
        elif op_name == 'PlusInf':
            return float('inf')
        elif op_name == 'NaN':
            return float('nan')
        else:
            raise BackendError("Called _abstract_fp_val with unknown type")

    def solver(self, timeout=None):
        s = z3.Solver(ctx=self._context)
        if timeout is not None:
            if 'soft_timeout' in str(s.param_descrs()):
                s.set('soft_timeout', timeout)
                s.set('solver2_timeout', timeout)
            else:
                s.set('timeout', timeout)
        _add_memory_pressure(1024 * 1024 * 10)
        return s

    def _add(self, s, c, track=False):
        if track:
            for constraint in c:
                name = str(hash(constraint))
                self._hash_to_constraint[name] = constraint
                s.assert_and_track(constraint, name)
        else:
            s.add(*c)

    def _unsat_core(self, s):
        cores = s.unsat_core()
        constraints = [ ]
        for core in cores:
            constraints.append(self._hash_to_constraint.get(str(core)))
        return constraints

    @condom
    def _primitive_from_model(self, model, expr):
        v = model.eval(expr, model_completion=True)
        return self._abstract_to_primitive(v.ctx.ctx, v.ast)

    #
    # New, model-driven solves
    #

    def _generic_model(self, z3_model):
        """
        Converts a Z3 model to a name->primitive dict.
        """
        model = { }
        for m_f in z3_model:
            n = m_f.name()
            m = m_f()
            me = z3_model.eval(m)
            model[n] = self._abstract_to_primitive(me.ctx.ctx, me.ast)

        return model

    def _satisfiable(self, extra_constraints=(), solver=None, model_callback=None):
        global solve_count

        solve_count += 1
        if len(extra_constraints) > 0:
            solver.push()
            solver.add(*extra_constraints)

        try:

            l.debug("Doing a check!")
            #print "CHECKING"
            if solver.check() != z3.sat:
                return False

            if model_callback is not None:
                model_callback(self._generic_model(solver.model()))
        finally:
            if len(extra_constraints) > 0:
                solver.pop()
        return True

    def _eval(self, expr, n, extra_constraints=(), solver=None, model_callback=None):
        results = self._batch_eval(
            [ expr ], n, extra_constraints=extra_constraints,
            solver=solver, model_callback=model_callback
        )

        # Unpack it
        return [ v[0] for v in results ]

    @condom
    def _batch_eval(self, exprs, n, extra_constraints=(), solver=None, model_callback=None):
        global solve_count

        result_values = [ ]

        if len(extra_constraints) > 0 or n != 1:
            solver.push()
        if len(extra_constraints) > 0:
            solver.add(*extra_constraints)

        for i in range(n):
            solve_count += 1
            l.debug("Doing a check!")
            if solver.check() != z3.sat:
                break
            model = solver.model()

            # construct results
            r = [ ]
            for expr in exprs:
                if not isinstance(expr, (numbers.Number, str, bool)):
                    v = self._primitive_from_model(model, expr)
                    r.append(v)
                else:
                    r.append(expr)

            # Append the solution to the result list
            if model_callback is not None:
                model_callback(self._generic_model(solver.model()))
            result_values.append(tuple(r))

            # Construct the extra constraint so we don't get the same result anymore
            if i + 1 != n:
                if len(exprs) == 1:
                    solver.add(exprs[0] != r[0])
                else:
                    solver.add(self._op_raw_Not(self._op_raw_And(*[(ex == ex_v) for ex, ex_v in zip(exprs, r)])))
                model = None

        if len(extra_constraints) > 0 or n != 1:
            solver.pop()

        return result_values

    @condom
    def _min(self, expr, extra_constraints=(), solver=None, model_callback=None):
        global solve_count

        lo = 0
        hi = 2**expr.size()-1
        vals = set()

        if len(extra_constraints) > 0:
            solver.push()
            solver.add(*[self.convert(e) for e in extra_constraints])

        numpop = 0

        # TODO: Can only deal with bitvectors, not floats
        while hi-lo > 1:
            middle = (lo + hi)//2
            #l.debug("h/m/l/d: %d %d %d %d", hi, middle, lo, hi-lo)

            solver.push()
            solver.add(z3.UGE(expr, lo), z3.ULE(expr, middle))
            numpop += 1

            solve_count += 1
            l.debug("Doing a check!")
            if solver.check() == z3.sat:
                l.debug("... still sat")
                if model_callback is not None:
                    model_callback(self._generic_model(solver.model()))
                vals.add(self._primitive_from_model(solver.model(), expr))
                hi = middle
            else:
                l.debug("... now unsat")
                lo = middle
                solver.pop()
                numpop -= 1

        for _ in range(numpop):
            solver.pop()

        #l.debug("final hi/lo: %d, %d", hi, lo)

        if hi == lo:
            vals.add(lo)
        else:
            solver.push()
            solver.add(expr == lo)
            l.debug("Doing a check!")
            if solver.check() == z3.sat:
                if model_callback is not None:
                    model_callback(self._generic_model(solver.model()))
                vals.add(lo)
                solver.pop()
            else:
                vals.add(hi)
                solver.pop()

        if len(extra_constraints) > 0:
            solver.pop()

        return min(vals)

    @condom
    def _max(self, expr, extra_constraints=(), solver=None, model_callback=None):
        global solve_count

        lo = 0
        hi = 2**expr.size()-1
        vals = set()

        if len(extra_constraints) > 0:
            solver.push()
            solver.add(*[self.convert(e) for e in extra_constraints])

        numpop = 0

        # TODO: Can only deal with bitvectors, not floats
        while hi-lo > 1:
            middle = (lo + hi)//2
            #l.debug("h/m/l/d: %d %d %d %d", hi, middle, lo, hi-lo)

            solver.push()
            solver.add(z3.UGT(expr, middle), z3.ULE(expr, hi))
            numpop += 1

            solve_count += 1
            l.debug("Doing a check!")
            if solver.check() == z3.sat:
                l.debug("... still sat")
                lo = middle
                vals.add(self._primitive_from_model(solver.model(), expr))
                if model_callback is not None:
                    model_callback(self._generic_model(solver.model()))
            else:
                l.debug("... now unsat")
                hi = middle
                solver.pop()
                numpop -= 1
            #l.debug("          now: %d %d %d %d", hi, middle, lo, hi-lo)

        for _ in range(numpop):
            solver.pop()

        if hi == lo:
            vals.add(hi)
        else:
            solver.push()
            solver.add(expr == hi)
            l.debug("Doing a check!")
            if solver.check() == z3.sat:
                if model_callback is not None:
                    model_callback(self._generic_model(solver.model()))
                vals.add(hi)
                solver.pop()
            else:
                vals.add(lo)
                solver.pop()

        if len(extra_constraints) > 0:
            solver.pop()

        return max(vals)

    def _simplify(self, expr): #pylint:disable=W0613,R0201
        raise Exception("This shouldn't be called. Bug Yan.")

    @condom
    def simplify(self, expr):
        if expr._simplified:
            return expr

        if self._enable_simplification_cache:
            try:
                k = self._simplification_cache_key[expr._cache_key]
                #print "HIT WEAK KEY CACHE"
                return k
            except KeyError:
                pass
            try:
                k = self._simplification_cache_val[expr._cache_key]
                #print "HIT WEAK VALUE CACHE"
                return k
            except KeyError:
                pass

            #print "MISS CACHE"

        l.debug("SIMPLIFYING EXPRESSION")

        #print "SIMPLIFYING"

        expr_raw = self.convert(expr)

        #l.debug("... before: %s (%s)", expr_raw, expr_raw.__class__.__name__)

        #s = expr_raw
        if isinstance(expr_raw, z3.BoolRef):
            boolref_tactics = self._boolref_tactics
            s = boolref_tactics(expr_raw).as_expr()
            #n = s.decl().name()
            #if n == 'true':
            #    s = True
            #elif n == 'false':
            #    s = False
        elif isinstance(expr_raw, z3.BitVecRef):
            s = z3.simplify(expr_raw)
        else:
            s = expr_raw

        o = self._abstract(s)
        o._simplified = Base.FULL_SIMPLIFY

        if self._enable_simplification_cache:
            self._simplification_cache_val[expr._cache_key] = o
            self._simplification_cache_key[expr._cache_key] = o
        return o

    def _is_false(self, e, extra_constraints=(), solver=None, model_callback=None):
        return z3.simplify(e).eq(z3.BoolVal(False, ctx=self._context))

    def _is_true(self, e, extra_constraints=(), solver=None, model_callback=None):
        return z3.simplify(e).eq(z3.BoolVal(True, ctx=self._context))

    def _solution(self, expr, v, extra_constraints=(), solver=None, model_callback=None):
        return self._satisfiable(extra_constraints=(expr == v,) + tuple(extra_constraints), solver=solver, model_callback=model_callback)

    #
    # Some Z3 passthroughs
    #

    # these require the context or special treatment

    @staticmethod
    def _op_div(a, b):
        return z3.UDiv(a, b)
    @staticmethod
    def _op_mod(a, b):
        return z3.URem(a, b)
    @staticmethod
    def _op_add(*args):
        return reduce(operator.__add__, args)
    @staticmethod
    def _op_sub(*args):
        return reduce(operator.__sub__, args)
    @staticmethod
    def _op_mul(*args):
        return reduce(operator.__mul__, args)
    @staticmethod
    def _op_or(*args):
        return reduce(operator.__or__, args)
    @staticmethod
    def _op_xor(*args):
        return reduce(operator.__xor__, args)
    @staticmethod
    def _op_and(*args):
        return reduce(operator.__and__, args)

    def _op_raw_And(self, *args):
        return z3.And(*(tuple(args) + ( self._context, )))

    def _op_raw_Xor(self, *args):
        return z3.Xor(*(tuple(args) + ( self._context, )))

    def _op_raw_Or(self, *args):
        return z3.Or(*(tuple(args) + ( self._context, )))

    def _op_raw_Not(self, a):
        return z3.Not(a, ctx=self._context)

    def _op_raw_If(self, i, t, e):
        return z3.If(i, t, e, ctx=self._context)

    @condom
    def _op_raw_fpAbs(self, a):
        return z3.fpAbs(a, ctx=self._context)

    @condom
    def _op_raw_fpNeg(self, a):
        return z3.fpNeg(a, ctx=self._context)

    @condom
    def _op_raw_fpAdd(self, rm, a, b):
        return z3.fpAdd(rm, a, b, ctx=self._context)

    @condom
    def _op_raw_fpSub(self, rm, a, b):
        return z3.fpSub(rm, a, b, ctx=self._context)

    @condom
    def _op_raw_fpMul(self, rm, a, b):
        return z3.fpMul(rm, a, b, ctx=self._context)

    @condom
    def _op_raw_fpDiv(self, rm, a, b):
        return z3.fpDiv(rm, a, b, ctx=self._context)

    @condom
    def _op_raw_fpLT(self, a, b):
        return z3.fpLT(a, b, ctx=self._context)

    @condom
    def _op_raw_fpLEQ(self, a, b):
        return z3.fpLEQ(a, b, ctx=self._context)

    @condom
    def _op_raw_fpGT(self, a, b):
        return z3.fpGT(a, b, ctx=self._context)

    @condom
    def _op_raw_fpGEQ(self, a, b):
        return z3.fpGEQ(a, b, ctx=self._context)

    @condom
    def _op_raw_fpEQ(self, a, b):
        return z3.fpEQ(a, b, ctx=self._context)

    @condom
    def _op_raw_fpFP(self, sgn, exp, sig):
        return z3.fpFP(sgn, exp, sig, ctx=self._context)

    @condom
    def _op_raw_fpToSBV(self, rm, fp, bv_len):
        return z3.fpToSBV(rm, fp, z3.BitVecSort(bv_len, ctx=self._context))

    @condom
    def _op_raw_fpToUBV(self, rm, fp, bv_len):
        return z3.fpToUBV(rm, fp, z3.BitVecSort(bv_len, ctx=self._context))

    @condom
    def _op_raw_fpToFP(self, a1, a2=None, a3=None):
        return z3.fpToFP(a1, a2=a2, a3=a3, ctx=self._context)

    @condom
    def _op_raw_fpToIEEEBV(self, x):
        return z3.fpToIEEEBV(x, ctx=self._context)

    # and these do not
    _op_raw_Concat = _raw_caller(z3.Concat)
    _op_raw_Extract = _raw_caller(z3.Extract)
    _op_raw_LShR = _raw_caller(z3.LShR)
    _op_raw_RotateLeft = _raw_caller(z3.RotateLeft)
    _op_raw_RotateRight = _raw_caller(z3.RotateRight)
    _op_raw_SignExt = _raw_caller(z3.SignExt)
    _op_raw_UGE = _raw_caller(z3.UGE)
    _op_raw_UGT = _raw_caller(z3.UGT)
    _op_raw_ULE = _raw_caller(z3.ULE)
    _op_raw_ULT = _raw_caller(z3.ULT)
    _op_raw_ZeroExt = _raw_caller(z3.ZeroExt)
    _op_raw_SMod = _raw_caller(z3.SRem)

    @staticmethod
    @condom
    def _op_raw_Reverse(a):
        if a.size() == 8:
            return a
        elif a.size() % 8 != 0:
            raise ClaripyOperationError("can't reverse non-byte sized bitvectors")
        else:
            return z3.Concat(*[z3.Extract(i+7, i, a) for i in range(0, a.size(), 8)])

    @staticmethod
    @condom
    def _op_raw_SLT(a, b):
        return a < b

    @staticmethod
    @condom
    def _op_raw_SLE(a, b):
        return a <= b

    @staticmethod
    @condom
    def _op_raw_SGT(a, b):
        return a > b

    @staticmethod
    @condom
    def _op_raw_SGE(a, b):
        return a >= b

    @staticmethod
    @condom
    def _op_raw_SDiv(a, b):
        return a / b

    def _identical(self, a, b):
        return a.eq(b)

#
# this is for the actual->abstract conversion above
#

# these are Z3 operation names for abstraction
z3_op_names = [ _ for _ in dir(z3) if _.startswith('Z3_OP') ]
z3_op_nums = { getattr(z3, _): _ for _ in z3_op_names }

#pylint:disable=bad-continuation
op_map = {
    # Boolean
    'Z3_OP_TRUE': 'True',
    'Z3_OP_FALSE': 'False',
    'Z3_OP_EQ': '__eq__',
    'Z3_OP_DISTINCT': '__ne__',
    'Z3_OP_ITE': 'If',
    'Z3_OP_AND': 'And',
    'Z3_OP_OR': 'Or',
    'Z3_OP_IFF': '__eq__',
    'Z3_OP_XOR': 'Xor',
    'Z3_OP_NOT': 'Not',
    'Z3_OP_IMPLIES': 'Implies',
    #'Z3_OP_OEQ': None,

    # Arithmetic
    #'Z3_OP_ANUM': None,
    #'Z3_OP_AGNUM': None,
    'Z3_OP_LE': 'SLE',
    'Z3_OP_GE': 'SGE',
    'Z3_OP_LT': 'SLT',
    'Z3_OP_GT': 'SGT',
    'Z3_OP_ADD': '__add__',
    'Z3_OP_SUB': '__sub__',
    'Z3_OP_UMINUS': '__neg__',
    'Z3_OP_MUL': '__mul__',
    'Z3_OP_DIV': 'SDiv',
    'Z3_OP_IDIV': 'SDiv',
    'Z3_OP_REM': '__mod__',
    'Z3_OP_MOD': '__mod__',
    #'Z3_OP_TO_REAL': None,
    #'Z3_OP_TO_INT': None,
    #'Z3_OP_IS_INT': None,
    'Z3_OP_POWER': '__pow__',

    # Arrays & Sets
    #'Z3_OP_STORE': None,
    #'Z3_OP_SELECT': None,
    #'Z3_OP_CONST_ARRAY': None,
    #'Z3_OP_ARRAY_MAP': None,
    #'Z3_OP_ARRAY_DEFAULT': None,
    #'Z3_OP_SET_UNION': None,
    #'Z3_OP_SET_INTERSECT': None,
    #'Z3_OP_SET_DIFFERENCE': None,
    #'Z3_OP_SET_COMPLEMENT': None,
    #'Z3_OP_SET_SUBSET': None,
    #'Z3_OP_AS_ARRAY': None,

    # Bit-vectors
    'Z3_OP_BNUM': 'BitVecVal',
    #'Z3_OP_BIT1': None, # MAYBE TODO
    #'Z3_OP_BIT0': None, # MAYBE TODO
    'Z3_OP_BNEG': '__neg__',
    'Z3_OP_BADD': '__add__',
    'Z3_OP_BSUB': '__sub__',
    'Z3_OP_BMUL': '__mul__',

    'Z3_OP_BSDIV': 'SDiv',
    'Z3_OP_BUDIV': '__floordiv__',
    'Z3_OP_BSREM': 'SMod',
    'Z3_OP_BUREM': '__mod__',
    'Z3_OP_BSMOD': 'SMod',
    'Z3_OP_BSDIV_I': 'SDiv',
    'Z3_OP_BUDIV_I': '__floordiv__',
    'Z3_OP_BSREM_I': 'SMod',
    'Z3_OP_BUREM_I': '__mod__',
    'Z3_OP_BSMOD_I': 'SMod',

    # special functions to record the division by 0 cases
    # these are internal functions
    #'Z3_OP_BSDIV0': None,
    #'Z3_OP_BUDIV0': None,
    #'Z3_OP_BSREM0': None,
    #'Z3_OP_BUREM0': None,
    #'Z3_OP_BSMOD0': None,

    'Z3_OP_ULEQ': 'ULE',
    'Z3_OP_SLEQ': 'SLE',
    'Z3_OP_UGEQ': 'UGE',
    'Z3_OP_SGEQ': 'SGE',
    'Z3_OP_ULT': 'ULT',
    'Z3_OP_SLT': 'SLT',
    'Z3_OP_UGT': 'UGT',
    'Z3_OP_SGT': 'SGT',

    'Z3_OP_BAND': '__and__',
    'Z3_OP_BOR': '__or__',
    'Z3_OP_BNOT': '__invert__',
    'Z3_OP_BXOR': '__xor__',
    #'Z3_OP_BNAND': None,
    #'Z3_OP_BNOR': None,
    #'Z3_OP_BXNOR': None,

    'Z3_OP_CONCAT': 'Concat',
    'Z3_OP_SIGN_EXT': 'SignExt',
    'Z3_OP_ZERO_EXT': 'ZeroExt',
    'Z3_OP_EXTRACT': 'Extract',
    'Z3_OP_REPEAT': 'RepeatBitVec',

    #'Z3_OP_BREDOR': None,
    #'Z3_OP_BREDAND': None,
    #'Z3_OP_BCOMP': None,

    'Z3_OP_BSHL': '__lshift__',
    'Z3_OP_BLSHR': 'LShR',
    'Z3_OP_BASHR': '__rshift__',
    #'Z3_OP_ROTATE_LEFT': None,
    #'Z3_OP_ROTATE_RIGHT': None,
    'Z3_OP_EXT_ROTATE_LEFT': 'RotateLeft',
    'Z3_OP_EXT_ROTATE_RIGHT': 'RotateRight',

    'Z3_OP_FPA_TO_SBV': 'fpToSBV',
    'Z3_OP_FPA_TO_UBV': 'fpToUBV',
    'Z3_OP_FPA_TO_IEEE_BV': 'fpToIEEEBV',
    'Z3_OP_FPA_TO_FP': 'fpToFP',
    'Z3_OP_FPA_NUM': 'FPVal',

    'Z3_OP_FPA_MINUS_ZERO': 'MinusZero',
    'Z3_OP_FPA_MINUS_INF': 'MinusInf',
    'Z3_OP_FPA_PLUS_ZERO': 'PlusZero',
    'Z3_OP_FPA_PLUS_INF': 'PlusInf',
    'Z3_OP_FPA_NAN': 'NaN',

    'Z3_OP_FPA_EQ': 'fpEQ',
    'Z3_OP_FPA_GT': 'fpGT',
    'Z3_OP_FPA_GE': 'fpGEQ',
    'Z3_OP_FPA_LT': 'fpLT',
    'Z3_OP_FPA_LE': 'fpLEQ',

    'Z3_OP_FPA_ABS': 'fpAbs',
    'Z3_OP_FPA_NEG': 'fpNeg',
    'Z3_OP_FPA_ADD': 'fpAdd',
    'Z3_OP_FPA_SUB': 'fpSub',
    'Z3_OP_FPA_MUL': 'fpMul',
    'Z3_OP_FPA_DIV': 'fpDiv',

    'Z3_OP_FPA_RM_NEAREST_TIES_TO_EVEN': 'RM_RNE',
    'Z3_OP_FPA_RM_NEAREST_TIES_TO_AWAY': 'RM_RNA',
    'Z3_OP_FPA_RM_TOWARD_ZERO': 'RM_RTZ',
    'Z3_OP_FPA_RM_TOWARD_POSITIVE': 'RM_RTP',
    'Z3_OP_FPA_RM_TOWARD_NEGATIVE': 'RM_RTN',

    'Z3_OP_UNINTERPRETED': 'UNINTERPRETED'
}

from ..ast.base import Base
from ..ast.bv import BV, BVV
from ..ast.bool import BoolV, Bool
from ..ast.fp import FP, FPV
from ..operations import backend_operations, backend_fp_operations
from ..fp import FSort, RM, RM_RNE, RM_RNA, RM_RTP, RM_RTN, RM_RTZ
from ..errors import ClaripyError, BackendError, ClaripyOperationError
from .. import _all_operations

op_type_map = {
    # Boolean
    'Z3_OP_TRUE': Bool,
    'Z3_OP_FALSE': Bool,
    'Z3_OP_EQ': Bool,
    'Z3_OP_DISTINCT': Bool,
    'Z3_OP_ITE': Bool,
    'Z3_OP_AND': Bool,
    'Z3_OP_OR': Bool,
    'Z3_OP_IFF': Bool,
    'Z3_OP_XOR': Bool,
    'Z3_OP_NOT': Bool,
    'Z3_OP_IMPLIES': Bool,
    #'Z3_OP_OEQ': None,

    # Arithmetic
    #'Z3_OP_ANUM': None,
    #'Z3_OP_AGNUM': None,
    'Z3_OP_LE': None,
    'Z3_OP_GE': None,
    'Z3_OP_LT': None,
    'Z3_OP_GT': None,
    'Z3_OP_ADD': None,
    'Z3_OP_SUB': None,
    'Z3_OP_UMINUS': None,
    'Z3_OP_MUL': None,
    'Z3_OP_DIV': None,
    'Z3_OP_IDIV': None,
    'Z3_OP_REM': None, # TODO: is this correct?
    'Z3_OP_MOD': None,
    #'Z3_OP_TO_REAL': None,
    #'Z3_OP_TO_INT': None,
    #'Z3_OP_IS_INT': None,
    'Z3_OP_POWER': None,

    # Arrays & Sets
    #'Z3_OP_STORE': None,
    #'Z3_OP_SELECT': None,
    #'Z3_OP_CONST_ARRAY': None,
    #'Z3_OP_ARRAY_MAP': None,
    #'Z3_OP_ARRAY_DEFAULT': None,
    #'Z3_OP_SET_UNION': None,
    #'Z3_OP_SET_INTERSECT': None,
    #'Z3_OP_SET_DIFFERENCE': None,
    #'Z3_OP_SET_COMPLEMENT': None,
    #'Z3_OP_SET_SUBSET': None,
    #'Z3_OP_AS_ARRAY': None,

    # Bit-vectors
    'Z3_OP_BNUM': 'BitVecVal',
    #'Z3_OP_BIT1': None, # MAYBE TODO
    #'Z3_OP_BIT0': None, # MAYBE TODO
    'Z3_OP_BNEG': BV,
    'Z3_OP_BADD': BV,
    'Z3_OP_BSUB': BV,
    'Z3_OP_BMUL': BV,

    'Z3_OP_BSDIV': BV,
    'Z3_OP_BUDIV': BV,
    'Z3_OP_BSREM': BV,
    'Z3_OP_BUREM': BV,
    'Z3_OP_BSMOD': BV,
    'Z3_OP_BSDIV_I': BV,
    'Z3_OP_BUDIV_I': BV,
    'Z3_OP_BSREM_I': BV,
    'Z3_OP_BUREM_I': BV,
    'Z3_OP_BSMOD_I': BV,

    # special functions to record the division by 0 cases
    # these are internal functions
    #'Z3_OP_BSDIV0': None,
    #'Z3_OP_BUDIV0': None,
    #'Z3_OP_BSREM0': None,
    #'Z3_OP_BUREM0': None,
    #'Z3_OP_BSMOD0': None,

    'Z3_OP_ULEQ': Bool,
    'Z3_OP_SLEQ': Bool,
    'Z3_OP_UGEQ': Bool,
    'Z3_OP_SGEQ': Bool,
    'Z3_OP_ULT': Bool,
    'Z3_OP_SLT': Bool,
    'Z3_OP_UGT': Bool,
    'Z3_OP_SGT': Bool,

    'Z3_OP_BAND': BV,
    'Z3_OP_BOR': BV,
    'Z3_OP_BNOT': BV,
    'Z3_OP_BXOR': BV,
    #'Z3_OP_BNAND': None,
    #'Z3_OP_BNOR': None,
    #'Z3_OP_BXNOR': None,

    'Z3_OP_CONCAT': BV,
    'Z3_OP_SIGN_EXT': BV,
    'Z3_OP_ZERO_EXT': BV,
    'Z3_OP_EXTRACT': BV,
    'Z3_OP_REPEAT': BV,

    #'Z3_OP_BREDOR': None,
    #'Z3_OP_BREDAND': None,
    #'Z3_OP_BCOMP': None,

    'Z3_OP_BSHL': BV,
    'Z3_OP_BLSHR': BV,
    'Z3_OP_BASHR': BV,
    #'Z3_OP_ROTATE_LEFT': None,
    #'Z3_OP_ROTATE_RIGHT': None,
    'Z3_OP_EXT_ROTATE_LEFT': BV,
    'Z3_OP_EXT_ROTATE_RIGHT': BV,

    'Z3_OP_FPA_TO_SBV': BV,
    'Z3_OP_FPA_TO_UBV': BV,
    'Z3_OP_FPA_TO_IEEE_BV': BV,
    'Z3_OP_FPA_TO_FP': FP,
    'Z3_OP_FPA_NUM': FP,

    'Z3_OP_FPA_MINUS_ZERO': FP,
    'Z3_OP_FPA_MINUS_INF': FP,
    'Z3_OP_FPA_PLUS_ZERO': FP,
    'Z3_OP_FPA_PLUS_INF': FP,
    'Z3_OP_FPA_NAN': FP,

    'Z3_OP_FPA_EQ': Bool,
    'Z3_OP_FPA_GT': Bool,
    'Z3_OP_FPA_GE': Bool,
    'Z3_OP_FPA_LT': Bool,
    'Z3_OP_FPA_LE': Bool,

    'Z3_OP_FPA_ABS': FP,
    'Z3_OP_FPA_NEG': FP,
    'Z3_OP_FPA_ADD': FP,
    'Z3_OP_FPA_SUB': FP,
    'Z3_OP_FPA_MUL': FP,
    'Z3_OP_FPA_DIV': FP,

    'Z3_OP_UNINTERPRETED': 'UNINTERPRETED'
}
