
from typing import Optional
import logging

import ailment

from ...calling_conventions import SimRegArg
from ...sim_type import SimTypeFunction
from ...engines.light import SimEngineLightAILMixin, SpOffset
from ..typehoon import typeconsts, typevars
from ..typehoon.lifter import TypeLifter
from .engine_base import SimEngineVRBase, RichR

l = logging.getLogger(name=__name__)


class SimEngineVRAIL(
    SimEngineLightAILMixin,
    SimEngineVRBase,
):

    # Statement handlers

    def _ail_handle_Assignment(self, stmt):
        dst_type = type(stmt.dst)

        if dst_type is ailment.Expr.Register:
            offset = stmt.dst.reg_offset
            data = self._expr(stmt.src)
            size = stmt.src.bits // 8

            self._assign_to_register(offset, data, size, src=stmt.src, dst=stmt.dst)

        elif dst_type is ailment.Expr.Tmp:
            # simply write to self.tmps
            data = self._expr(stmt.src)
            if data is None:
                return

            self.tmps[stmt.dst.tmp_idx] = data

        else:
            l.warning('Unsupported dst type %s.', dst_type)

    def _ail_handle_Store(self, stmt):
        addr_r = self._expr(stmt.addr)
        data = self._expr(stmt.data)
        size = stmt.data.bits // 8

        self._store(addr_r, data, size, stmt=stmt)

    def _ail_handle_Jump(self, stmt):
        pass

    def _ail_handle_ConditionalJump(self, stmt):
        self._expr(stmt.condition)

    def _ail_handle_Call(self, stmt):
        target = stmt.target
        args = [ ]
        if stmt.args:
            for arg in stmt.args:
               args.append(self._expr(arg))

        ret_expr: Optional[ailment.Expr.Register] = stmt.ret_expr
        if ret_expr is not None:
            ret_reg_offset = ret_expr.reg_offset
        else:
            if stmt.calling_convention is not None:
                # return value
                ret_expr: SimRegArg = stmt.calling_convention.RETURN_VAL
            else:
                l.debug("Unknown calling convention for function %s. Fall back to default calling convention.", target)
                ret_expr: SimRegArg = self.project.factory.cc().RETURN_VAL
            ret_reg_offset = self.project.arch.registers[ret_expr.reg_name][0]

        # discover the prototype
        prototype: Optional[SimTypeFunction] = None
        if stmt.calling_convention is not None:
            prototype = stmt.calling_convention.func_ty
        elif isinstance(stmt.target, ailment.Expr.Const):
            func_addr = stmt.target.value
            if func_addr in self.kb.functions:
                func = self.kb.functions[func_addr]
                prototype = func.prototype

        if ret_expr is not None:
            # dump the type of the return value
            if prototype is not None:
                ret_ty = TypeLifter(self.arch.bits).lift(prototype.returnty)
            else:
                ret_ty = None
            if isinstance(ret_ty, typeconsts.BottomType):
                ret_ty = None

            self._assign_to_register(
                ret_reg_offset,
                RichR(None, typevar=ret_ty),
                self.state.arch.bytes,
                dst=ret_expr,
            )

        if prototype is not None and args:
            # add type constraints
            for arg, arg_type in zip(args, prototype.args):
                arg_ty = TypeLifter(self.arch.bits).lift(arg_type)
                type_constraint = typevars.Subtype(
                    arg_ty, arg.typevar
                )
                self.state.add_type_constraint(type_constraint)

    # Expression handlers

    def _expr(self, expr):
        """

        :param expr:
        :return:
        :rtype: RichR
        """

        expr = super()._expr(expr)
        if expr is None:
            return RichR(None)
        return expr

    def _ail_handle_Register(self, expr):
        offset = expr.reg_offset
        size = expr.bits // 8

        return self._read_from_register(offset, size, expr=expr)

    def _ail_handle_Load(self, expr):
        addr_r = self._expr(expr.addr)
        size = expr.size

        return self._load(addr_r, size, expr=expr)

    def _ail_handle_Const(self, expr):
        return RichR(expr.value, typevar=typeconsts.int_type(expr.size * 8))

    def _ail_handle_BinaryOp(self, expr):
        r = super()._ail_handle_BinaryOp(expr)
        if r is None:
            # Treat it as a normal binaryop expression
            self._expr(expr.operands[0])
            self._expr(expr.operands[1])
            # still return a RichR instance
            r = RichR(None)
        return r

    def _ail_handle_Convert(self, expr: ailment.Expr.Convert):
        r = self._expr(expr.operand)
        typevar = None
        if r.typevar is not None:
            if isinstance(r.typevar, typevars.DerivedTypeVariable) and isinstance(r.typevar.label, typevars.ConvertTo):
                # there is already a conversion - overwrite it
                typevar = typevars.DerivedTypeVariable(r.typevar.type_var, typevars.ConvertTo(expr.to_bits))
            else:
                typevar = typevars.DerivedTypeVariable(r.typevar, typevars.ConvertTo(expr.to_bits))

        return RichR(r.data, typevar=typevar)

    def _ail_handle_StackBaseOffset(self, expr):
        return RichR(
            SpOffset(self.arch.bits, expr.offset, is_base=False)
        )

    def _ail_handle_ITE(self, expr: ailment.Expr.ITE):
        # pylint:disable=unused-variable
        cond = self._expr(expr.cond)
        r0 = self._expr(expr.iftrue)
        r1 = self._expr(expr.iffalse)

    def _ail_handle_Cmp(self, expr):  # pylint:disable=useless-return
        self._expr(expr.operands[0])
        self._expr(expr.operands[1])
        return RichR(None)

    _ail_handle_CmpEQ = _ail_handle_Cmp
    _ail_handle_CmpNE = _ail_handle_Cmp
    _ail_handle_CmpLT = _ail_handle_Cmp
    _ail_handle_CmpLE = _ail_handle_Cmp
    _ail_handle_CmpGT = _ail_handle_Cmp
    _ail_handle_CmpGE = _ail_handle_Cmp

    def _ail_handle_Add(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            typevar = None
            type_constraints = set()
            if r0.typevar is not None and isinstance(r1.data, int):
                # addition with constants. create a derived type variable
                typevar = typevars.DerivedTypeVariable(r0.typevar, typevars.AddN(r1.data))
            else:
                # create a new type variable and add constraints accordingly
                typevar = typevars.TypeVariable()
                type_constraints.add(typevars.Add(r0.typevar, r1.typevar, typevar))

            sum_ = None
            if r0.data is not None and r1.data is not None:
                sum_ = r0.data + r1.data

            type_constraints.add(typevars.Subtype(r0.typevar, r1.typevar))

            return RichR(sum_,
                         typevar=typevar,
                         type_constraints=type_constraints,
                         )
        except TypeError:
            return RichR(ailment.Expr.BinaryOp(expr.idx, 'Add', [r0, r1], **expr.tags))

    def _ail_handle_Sub(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            typevar = None
            if r0.typevar is not None and isinstance(r1.data, int):
                typevar = typevars.DerivedTypeVariable(r0.typevar, typevars.SubN(r1.data))

            sub = None
            if r0.data is not None and r1.data is not None:
                sub = r0.data - r1.data

            return RichR(sub,
                         typevar=typevar,
                         type_constraints={ typevars.Subtype(r0.typevar, r1.typevar) },
                         )
        except TypeError:
            return RichR(ailment.Expr.BinaryOp(expr.idx, 'Sub', [r0, r1], **expr.tags))

    def _ail_handle_Mul(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            if isinstance(r0.data, int) and isinstance(r1.data, int):
                # constants
                result_size = arg0.bits
                return RichR(r0.data * r1.data,
                             typevar=typeconsts.int_type(result_size),
                             type_constraints=None)

            remainder = None
            if r0.data is not None and r1.data is not None:
                remainder = r0.data * r1.data

            return RichR(remainder,
                         typevar=r0.typevar,
                         )
        except TypeError:
            return RichR(ailment.Expr.BinaryOp(expr.idx, 'Mul', [r0, r1], **expr.tags))

    def _ail_handle_Div(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            if isinstance(r0.data, int) and isinstance(r1.data, int):
                # constants
                result_size = arg0.bits
                return RichR(r0.data // r1.data,
                             typevar=typeconsts.int_type(result_size),
                             type_constraints=None)

            remainder = None
            if r0.data is not None and r1.data is not None:
                remainder = r0.data // r1.data

            return RichR(remainder,
                         typevar=r0.typevar,
                         )
        except TypeError:
            return RichR(ailment.Expr.BinaryOp(expr.idx, 'Div', [r0, r1], **expr.tags))

    def _ail_handle_Xor(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            if isinstance(r0.data, int) and isinstance(r1.data, int):
                # constants
                result_size = arg0.bits
                return RichR(r0.data ^ r1.data,
                             typevar=typeconsts.int_type(result_size),
                             type_constraints=None)

            remainder = None
            if r0.data is not None and r1.data is not None:
                remainder = r0.data ^ r1.data

            return RichR(remainder,
                         typevar=r0.typevar,
                         )
        except TypeError:
            return RichR(ailment.Expr.BinaryOp(expr.idx, 'Xor', [r0, r1], **expr.tags))

    def _ail_handle_Shl(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            if isinstance(r0.data, int) and isinstance(r1.data, int):
                # constants
                result_size = arg0.bits
                return RichR(r0.data << r1.data,
                             typevar=typeconsts.int_type(result_size),
                             type_constraints=None)

            r = None
            if r0.data is not None and r1.data is not None:
                r = r0.data << r1.data

            return RichR(r,
                         typevar=r0.typevar,
                         )

        except TypeError as ex:
            self.l.warning(ex)
            return RichR(None)

    def _ail_handle_Shr(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            if isinstance(r0.data, int) and isinstance(r1.data, int):
                # constants
                result_size = arg0.bits
                return RichR(r0.data >> r1.data,
                             typevar=typeconsts.int_type(result_size),
                             type_constraints=None)

            r = None
            if r0.data is not None and r1.data is not None:
                r = r0.data >> r1.data

            return RichR(r,
                         typevar=r0.typevar,
                         )

        except TypeError as ex:
            self.l.warning(ex)
            return RichR(None)

    def _ail_handle_Sal(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            if isinstance(r0.data, int) and isinstance(r1.data, int):
                result_size = arg0.bits
                # constants
                return RichR(r0.data << r1.data,
                             typevar=typeconsts.int_type(result_size),
                             type_constraints=None)

            r = None
            if r0.data is not None and r1.data is not None:
                r = r0.data << r1.data

            return RichR(r,
                         typevar=r0.typevar,
                         )

        except TypeError as ex:
            self.l.warning(ex)
            return RichR(None)

    def _ail_handle_Sar(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            if isinstance(r0.data, int) and isinstance(r1.data, int):
                # constants
                result_size = arg0.bits
                return RichR(r0.data >> r1.data,
                             typevar=typeconsts.int_type(result_size),
                             type_constraints=None)

            r = None
            if r0.data is not None and r1.data is not None:
                r = r0.data >> r1.data

            return RichR(r,
                         typevar=r0.typevar,
                         )

        except TypeError as ex:
            self.l.warning(ex)
            return RichR(None)

    def _ail_handle_And(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            if isinstance(r0.data, int) and isinstance(r1.data, int):
                result_size = arg0.bits
                return RichR(
                    r0.data & r1.data,
                    typevar=typeconsts.int_type(result_size),
                    type_constraints=None,
                )
            r = None
            if r0.data is not None and r1.data is not None:
                r = r0.data & r1.data
            return RichR(r, typevar=r0.typevar)

        except TypeError:
            self.l.warning("_ail_handle_And(): TypeError.", exc_info=True)
            return RichR(None)

    def _ail_handle_Or(self, expr):

        arg0, arg1 = expr.operands

        r0 = self._expr(arg0)
        r1 = self._expr(arg1)

        try:
            if isinstance(r0.data, int) and isinstance(r1.data, int):
                result_size = arg0.bits
                return RichR(
                    r0.data | r1.data,
                    typevar=typeconsts.int_type(result_size),
                    type_constraints=None,
                )
            r = None
            if r0.data is not None and r1.data is not None:
                r = r0.data | r1.data
            return RichR(r, typevar=r0.typevar)

        except TypeError:
            self.l.warning("_ail_handle_Or(): TypeError.", exc_info=True)
            return RichR(None)

    def _ail_handle_Not(self, expr):
        arg = expr.operands[0]
        expr = self._expr(arg)
        try:
            result_size = arg.bits
            mask = (1 << result_size) - 1
            if isinstance(expr.data, int):
                return RichR(
                    (~expr.data) & mask,
                    typevar=typeconsts.int_type(result_size),
                    type_constraints=None,
                )
            r = None
            if expr.data is not None:
                r = (~expr.data) & mask
            return RichR(r, typevar=expr.typevar)

        except TypeError:
            self.l.warning("_ail_handle_Not(): TypeError.", exc_info=True)
            return RichR(None)
