package analysis

import com.microsoft.z3._
import com.typesafe.scalalogging.StrictLogging
import constraints.Z3Constraint
import dex.DexType.{PrimitiveType, ObjectType, VoidType}
import dex.{DexClass, DexMethod, DexType}
import org.jf.dexlib2.dexbacked.reference.{
  DexBackedStringReference,
  DexBackedTypeReference
}
import org.jf.dexlib2.iface.instruction.Instruction
import org.jf.dexlib2.iface.instruction.formats._
import org.jf.dexlib2.iface.reference.MethodReference
import utils.Utils

object InstructionAnalyzer extends StrictLogging {

  /**
    * Do nothing. All registers at pp+ approximates the registers at pp.
    */
  def nop(i: Instruction, pp: PP, env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    Seq(
      Z3Constraint(
        env.ctx.mkAnd(r <= rPlus: _*),
        s"R($pp) <= R(${pp +})",
        pp
      )
    )
  }

  def const4(i: Instruction11n, pp: PP, env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    val rApprox = r.<=(i.getRegisterA)(rPlus)
    Seq(
      Z3Constraint(
        env.domains.primitiveDomain.approx(
          env.domains.primitiveDomain.toElement(i.getWideLiteral.toInt),
          rPlus.numbers(i.getRegisterA)
        ),
        s"${i.getWideLiteral} <= R(${pp +})(${i.getRegisterA})",
        pp
      ),
      Z3Constraint(
        env.ctx.mkAnd(rApprox: _*),
        s"R($pp) <={${i.getRegisterA}} R(${pp +})",
        pp
      )
    )
  }

  def constString(i: Instruction21c,
                  pp: PP,
                  env: Environment): Seq[Z3Constraint] = {
    constString(
      i.getRegisterA,
      i.getReference.asInstanceOf[DexBackedStringReference].getString,
      pp,
      env
    )
  }

  def constStringJumbo(i: Instruction31c,
                       pp: PP,
                       env: Environment): Seq[Z3Constraint] = {
    constString(
      i.getRegisterA,
      i.getReference.asInstanceOf[DexBackedStringReference].getString,
      pp,
      env
    )
  }

  /**
    * Make the register A approximates str.
    * Constant strings are treated like instances of Ljava/lang/String;
    */
  private def constString(registerA: Int,
                          str: String,
                          pp: PP,
                          env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    Seq(
      Z3Constraint(
        env.domains.referenceDomain.approx(
          env.domains.referenceDomain.toElement(pp.toString),
          rPlus.references(registerA)
        ),
        s"($pp) <= R(${pp +})($registerA)",
        pp
      ),
      Z3Constraint(
        env.ctx.mkAnd(r.<=(registerA)(rPlus): _*),
        s"R($pp) <={$registerA} R(${pp +})",
        pp
      )
    )
  }

  def iput(i: Instruction22c, pp: PP, env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    val rApprox = r.<=(rPlus)
    val fieldRef = i.getReference.toString
    val cName = Utils.classPrefix(fieldRef)
    val fieldName = Utils.removeClassPrefix(fieldRef)
    val fieldType = DexType(fieldName.substring(fieldName.indexOf(":") + 1))
    val classes = classAndSubclasses(env)(cName)
    val obj: FuncDecl = env.H.obj()
    val objApp = env.ctx.mkApp(obj, env.ctx.mkString(pp.toString))
    val accessor: FuncDecl = fieldType match {
      case PrimitiveType() =>
        env.z3Obj.primAccessor
    }
    val objFields =
      env.ctx.mkApp(accessor, objApp).asInstanceOf[ArrayExpr]
    val constraints: Seq[Z3Constraint] = classes.flatMap(
      c =>
        env.preAnalysisResult.objectCreation
          .filter(p => p._2 == c.name)
          .map(objPP => {
            Z3Constraint(
              env.ctx.mkImplies(
                env.ctx.mkAnd(
                  env.domains.referenceDomain.approx(
                    env.domains.referenceDomain.toElement(objPP._1.toString),
                    r.references(i.getRegisterB)
                  )
                ),
                fieldType match {
                  case PrimitiveType() =>
                    env.domains.primitiveDomain.approx(
                      r.numbers(i.getRegisterA),
                      env.ctx
                        .mkSelect(objFields, env.ctx.mkString(fieldName))
                        .asInstanceOf[env.domains.primitiveDomain.Element]
                    )
                }
              ),
              s"${objPP._1.toString} <= R($pp)(${i.getRegisterB}) => " +
                s"R($pp)(${i.getRegisterA}) <= H($objPP).$fieldName",
              pp
            )
          })
    )
    constraints ++ Seq(
      Z3Constraint(
        env.ctx.mkAnd(rApprox: _*),
        s"R($pp) <= R(${pp +})",
        pp
      )
    )
  }

  def sgetObject(i: Instruction21c,
                 pp: PP,
                 env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    val sFieldRef = i.getReference.toString
    val cName = Utils.classPrefix(sFieldRef)
    val sFieldName = Utils.removeClassPrefix(sFieldRef)
    val sFieldType = DexType(sFieldName.substring(sFieldName.indexOf(":") + 1))
    val dexClass = env.app.classes.find(c => c.name == cName)
    val (sFieldVal, sFieldTaint): (Expr, Expr) = dexClass match {
      case Some(c) =>
        val f = c.fields.find(dexField => dexField.name == sFieldName).get
        env.SH(c, f)
      case None =>
        // Unknown class, use top as value.
        sFieldType match {
          case PrimitiveType() =>
            (env.domains.primitiveDomain.top, env.domains.taintDomain.top)
          case ObjectType(_) =>
            (env.domains.referenceDomain.top, env.domains.taintDomain.top)
        }
    }
    Seq(
      Z3Constraint(
        sFieldType match {
          case PrimitiveType() =>
            env.domains.primitiveDomain.approx(
              sFieldVal.asInstanceOf[env.domains.primitiveDomain.Element],
              rPlus.numbers(i.getRegisterA)
            )
          case ObjectType(_) =>
            env.domains.referenceDomain.approx(
              sFieldVal.asInstanceOf[env.domains.referenceDomain.Element],
              rPlus.references(i.getRegisterA)
            )
        },
        s"SH($cName,$sFieldName) <= R(${pp +})(${i.getRegisterA})",
        pp
      ),
      Z3Constraint(
        env.domains.taintDomain.approx(
          sFieldTaint.asInstanceOf[env.domains.taintDomain.Element],
          rPlus.taints(i.getRegisterA)
        ),
        s"Taint R($pp)(${i.getRegisterA}) if SH($cName,$sFieldName) is tainted",
        pp
      ),
      Z3Constraint(
        env.ctx.mkAnd(r.<=(i.getRegisterA)(rPlus): _*),
        s"R($pp) <={${i.getRegisterA}} R(${pp +})",
        pp
      )
    )
  }

  def sputObject(i: Instruction21c,
                 pp: PP,
                 env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    val sFieldRef = i.getReference.toString
    val cName = Utils.classPrefix(sFieldRef)
    val sFieldName = Utils.removeClassPrefix(sFieldRef)
    val sFieldType = DexType(sFieldName.substring(sFieldName.indexOf(":") + 1))
    val dexClass = env.app.classes.find(c => c.name == cName)
    val (sFieldVal, sFieldTaint): (Expr, Expr) = dexClass match {
      case Some(c) =>
        val f = c.fields.find(dexField => dexField.name == sFieldName).get
        env.SH(c, f)
      case None =>
        // Unknown class, use top as value.
        sFieldType match {
          case PrimitiveType() =>
            (env.domains.primitiveDomain.top, env.domains.taintDomain.top)
          case ObjectType(_) =>
            (env.domains.referenceDomain.top, env.domains.taintDomain.top)
        }
    }
    Seq(
      Z3Constraint(
        sFieldType match {
          case PrimitiveType() =>
            env.domains.primitiveDomain.approx(
              r.numbers(i.getRegisterA),
              sFieldVal.asInstanceOf[env.domains.primitiveDomain.Element]
            )
          case ObjectType(_) =>
            env.domains.referenceDomain.approx(
              r.references(i.getRegisterA),
              sFieldVal.asInstanceOf[env.domains.referenceDomain.Element]
            )
        },
        s"R($pp)(${i.getRegisterA}) <= SH($cName,$sFieldName)",
        pp
      ),
      Z3Constraint(
        env.domains.taintDomain.approx(
          r.taints(i.getRegisterA),
          sFieldTaint.asInstanceOf[env.domains.taintDomain.Element]
        ),
        s"Taint SH($cName,$sFieldName) if R($pp)(${i.getRegisterA}) is tainted",
        pp
      ),
      Z3Constraint(
        env.ctx.mkAnd(r <= rPlus: _*),
        s"R($pp) <= R(${pp +})",
        pp
      )
    )
  }

  def move(i: Instruction12x, pp: PP, env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    val rApprox = r.<=(i.getRegisterA)(rPlus)
    Seq(
      Z3Constraint(
        env.ctx.mkAnd(
          r.<=(i.getRegisterB, i.getRegisterA)(rPlus): _*
        ),
        s"R($pp)(${i.getRegisterB}) <= R(${pp +})(${i.getRegisterA})",
        pp
      ),
      Z3Constraint(
        env.ctx.mkAnd(rApprox: _*),
        s"R($pp) <={${i.getRegisterA}} R(${pp +})",
        pp
      )
    )
  }

  def moveResult(i: Instruction11x,
                 pp: PP,
                 env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    val rApprox = r.<=(i.getRegisterA)(rPlus)
    Seq(
      Z3Constraint(
        env.ctx.mkAnd(
          r.<=(env.R.retIdx, i.getRegisterA)(rPlus): _*
        ),
        s"R($pp)(ret) <= R(${pp +})(${i.getRegisterA})",
        pp
      ),
      Z3Constraint(
        env.ctx.mkAnd(rApprox: _*),
        s"R($pp) <={${i.getRegisterA}} R(${pp +})",
        pp
      )
    )
  }

  def goto(i: Instruction10t, pp: PP, env: Environment): Seq[Z3Constraint] = {
    val ppGoto = pp.copy(pc = pp.pc + i.getCodeOffset)
    Seq(
      Z3Constraint(
        env.ctx.mkAnd(env.R(pp) <= env.R(ppGoto): _*),
        s"$pp: R($pp) <= R($ppGoto)",
        pp
      )
    )
  }

  def retVoid(i: Instruction10x,
              pp: PP,
              env: Environment): Seq[Z3Constraint] = {
    Seq()
  }

  def ret(i: Instruction11x, pp: PP, env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val mResult = env.M(pp.c, pp.m).get
    pp.m.returnType match {
      case PrimitiveType() =>
        Seq(
          Z3Constraint(
            env.domains.primitiveDomain.approx(
              r.numbers(i.getRegisterA),
              mResult.asInstanceOf[env.domains.primitiveDomain.Element]
            ),
            s"$pp: R($pp)(${i.getRegisterA}) <= M(${pp.c.name}, ${pp.m.name})",
            pp
          )
        )
    }
  }

  def ifEq(i: Instruction22t, pp: PP, env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val ppGoto = pp.copy(pc = pp.pc + i.getCodeOffset)
    Seq(
      Z3Constraint(
        env.ctx.mkAnd(r <= env.R(ppGoto): _*),
        s"$pp: R($pp) <= R(${pp +})",
        pp
      ),
      Z3Constraint(
        env.ctx.mkAnd(r <= env.R(pp +): _*),
        s"$pp: R($pp) <= R($ppGoto)",
        pp
      )
    )
  }

  def newInstance(i: Instruction21c,
                  pp: PP,
                  env: Environment): Seq[Z3Constraint] = {
    val r = env.R(pp)
    val objType = i.getReference.asInstanceOf[DexBackedTypeReference].getType
    val obj: FuncDecl = env.H.obj()
    val objApp = env.ctx.mkApp(obj, env.ctx.mkString(pp.toString))
    val objName =
      env.ctx.mkApp(env.z3Obj.nameAccessor, objApp).asInstanceOf[SeqExpr]
    val primFields =
      env.ctx.mkApp(env.z3Obj.primAccessor, objApp).asInstanceOf[ArrayExpr]
    val fieldsInitialization: List[Z3Constraint] =
      env.app.classes.find(c => c.name == objType) match {
        case Some(dexClass) =>
          dexClass.fields
            .map(dexField =>
              dexField.dexType match {
                case PrimitiveType() =>
                  Z3Constraint(
                    env.domains.primitiveDomain.approx(
                      env.domains.primitiveDomain.toElement(0),
                      env.ctx
                        .mkSelect(primFields, env.ctx.mkString(dexField.name))
                        .asInstanceOf[env.domains.primitiveDomain.Element]
                    ),
                    s"0 <= H($pp).${dexField.name}",
                    pp
                  )
                case ObjectType(_) =>
                  Z3Constraint(
                    env.ctx.mkTrue(),
                    "true",
                    pp
                  )
            })
            .toList
        case None => List()
      }
    val instanceClass = Z3Constraint(
      env.ctx.mkEq(objName, env.ctx.mkString(objType)),
      s"H($pp) = {'$objType',(f->_)^*}",
      pp
    )
    val rPlus = env.R(pp +)
    // Register A of pp+ contains the object pointer (pp).
    val storeReference =
      Z3Constraint(
        env.domains.referenceDomain.approx(
          env.domains.referenceDomain.toElement(pp.toString),
          rPlus.references(i.getRegisterA)
        ),
        s"($pp) <= R(${pp +})(${i.getRegisterA})",
        pp
      )
    val regApprox = Z3Constraint(
      env.ctx.mkAnd(r.<=(i.getRegisterA)(rPlus): _*),
      s"R($pp) <={${i.getRegisterA}} R(${pp +})",
      pp
    )
    Seq(
      instanceClass,
      storeReference,
      regApprox
    ) ++ fieldsInitialization
  }

  private def approxParam(env: Environment)(
      pp: PP,
      register: Int,
      dexClass: DexClass,
      dexMethod: DexMethod,
      paramIdx: Int
  ): Z3Constraint = {
    val r = env.R(pp)
    val ppMethod = PP(dexClass, dexMethod, 0)
    val rMethod = env.R(ppMethod)
    val methodParamIdx = dexMethod.localRegistersCount + paramIdx
    Z3Constraint(
      env.ctx.mkAnd(
        r.<=(register, methodParamIdx)(rMethod): _*
      ),
      s"R($pp)($register) <= R($ppMethod)($methodParamIdx)",
      pp
    )
  }

  private def approxParameters(env: Environment)(
      i: Instruction35c,
      pp: PP,
      dexClass: DexClass,
      dexMethod: DexMethod): List[Z3Constraint] = {
    var paramApprox: List[Z3Constraint] = List()
    val regCount = i.getRegisterCount
    if (regCount > 1) {
      paramApprox = approxParam(env)(pp, i.getRegisterD, dexClass, dexMethod, 0) :: paramApprox
      if (regCount > 2) {
        paramApprox = approxParam(env)(
          pp,
          i.getRegisterE,
          dexClass,
          dexMethod,
          1
        ) :: paramApprox
        if (regCount > 3) {
          paramApprox = approxParam(env)(
            pp,
            i.getRegisterF,
            dexClass,
            dexMethod,
            2
          ) :: paramApprox
          if (regCount > 4) {
            paramApprox = approxParam(env)(
              pp,
              i.getRegisterG,
              dexClass,
              dexMethod,
              3
            ) :: paramApprox
          }
        }
      }
    }
    paramApprox.reverse
  }

  private def checkParamLeak(env: Environment)(pp: PP,
                                               register: Int): Z3Constraint = {
    val r = env.R(pp)
    Z3Constraint(
      env.ctx.mkNot(
        env.domains.taintDomain.approx(
          env.domains.taintDomain.top,
          r.taints(register)
        )
      ),
      s"Check R($pp)($register) LEAK",
      pp
    )
  }

  private def checkParametersLeak(env: Environment)(
      i: Instruction35c,
      pp: PP
  ): List[Z3Constraint] = {
    var paramsLeak: List[Z3Constraint] = List()
    val regCount = i.getRegisterCount
    if (regCount > 1) {
      paramsLeak = checkParamLeak(env)(pp, i.getRegisterD) :: paramsLeak
      if (regCount > 2) {
        paramsLeak = checkParamLeak(env)(pp, i.getRegisterE) :: paramsLeak
        if (regCount > 3) {
          paramsLeak = checkParamLeak(env)(pp, i.getRegisterF) :: paramsLeak
          if (regCount > 4) {
            paramsLeak = checkParamLeak(env)(pp, i.getRegisterG) :: paramsLeak
          }
        }
      }
    }
    paramsLeak.reverse
  }

  private def taintRegisterBasedOnParam(env: Environment)(
      pp: PP,
      register: Int,
      paramRegister: Int): Z3Constraint = {
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    Z3Constraint(
      env.domains.taintDomain.approx(
        r.taints(paramRegister),
        rPlus.taints(register)
      ),
      s"Taint R(${pp +})($register) if R($pp)($paramRegister) is tainted",
      pp
    )
  }

  private def taintRegisterBasedOnParameters(env: Environment)(
      i: Instruction35c,
      pp: PP,
      register: Int
  ): List[Z3Constraint] = {
    var registerTaint: List[Z3Constraint] = List()
    val paramsCount = i.getRegisterCount
    if (paramsCount > 1) {
      registerTaint = taintRegisterBasedOnParam(env)(
        pp,
        register,
        i.getRegisterD) :: registerTaint
      if (paramsCount > 2) {
        registerTaint = taintRegisterBasedOnParam(env)(
          pp,
          register,
          i.getRegisterE) :: registerTaint
        if (paramsCount > 3) {
          registerTaint = taintRegisterBasedOnParam(env)(
            pp,
            register,
            i.getRegisterF) :: registerTaint
          if (paramsCount > 4) {
            registerTaint = taintRegisterBasedOnParam(env)(
              pp,
              register,
              i.getRegisterG) :: registerTaint
          }
        }
      }
    }
    registerTaint.reverse
  }

  private def taintResBasedOnParam(
      env: Environment)(pp: PP, register: Int): Z3Constraint = {
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    Z3Constraint(
      env.domains.taintDomain.approx(
        r.taints(register),
        rPlus.taints(env.R.retIdx)
      ),
      s"Taint R(${pp +})(ret) if R($pp)($register) is tainted",
      pp
    )
  }

  private def taintResultBasedOnParameters(env: Environment)(
      i: Instruction35c,
      pp: PP
  ): List[Z3Constraint] = {
    var resultTaint: List[Z3Constraint] = List()
    val paramsCount = i.getRegisterCount
    if (paramsCount > 1) {
      resultTaint = taintResBasedOnParam(env)(pp, i.getRegisterD) :: resultTaint
      if (paramsCount > 2) {
        resultTaint = taintResBasedOnParam(env)(pp, i.getRegisterE) :: resultTaint
        if (paramsCount > 3) {
          resultTaint = taintResBasedOnParam(env)(pp, i.getRegisterF) :: resultTaint
          if (paramsCount > 4) {
            resultTaint = taintResBasedOnParam(env)(pp, i.getRegisterG) :: resultTaint
          }
        }
      }
    }
    resultTaint.reverse
  }

  def invokeDirect(
      i: Instruction35c,
      pp: PP,
      env: Environment,
      addMethodToAnalyze: (DexClass, DexMethod) => Unit): Seq[Z3Constraint] = {
    val mRef = i.getReference.asInstanceOf[MethodReference]
    // RegisterD: parameter
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    val constraints: Seq[Z3Constraint] =
      env.app.classes.find(c => c.name == mRef.getDefiningClass) match {
        case Some(dexClass) =>
          val method =
            dexClass.methods
              .find(m => m.name == Utils.removeClassPrefix(mRef.toString))
              .get
          addMethodToAnalyze(dexClass, method)
          val ppMethod = PP(dexClass, method, 0)
          val thisApprox = Z3Constraint(
            env.ctx.mkAnd(
              r.<=(
                i.getRegisterC,
                method.registersCount
              )(env.R(ppMethod)): _*
            ),
            s"R($pp)(${i.getRegisterC}) <= R($ppMethod)(${method.registersCount})",
            pp
          )
          val paramApprox = approxParameters(env)(i, pp, dexClass, method)
          val returnApprox: Z3Constraint =
            Z3Constraint(
              env.ctx.mkAnd(
                env
                  .M(dexClass, method)
                  .map(mResult => {
                    method.returnType match {
                      case PrimitiveType() =>
                        env.domains.primitiveDomain.approx(
                          mResult.asInstanceOf[env.domains.primitiveDomain.Element],
                          rPlus
                            .numbers(env.R.retIdx)
                        )
                    }
                  })
                  .getOrElse(env.ctx.mkTrue())
              ),
              s"M(${dexClass.name},${method.name}) <= R(${pp +})(ret)",
              pp
            )
          (thisApprox :: paramApprox) ++ Seq(returnApprox)
        case None =>
          taintRegisterBasedOnParameters(env)(i, pp, i.getRegisterC) ++
            taintResultBasedOnParameters(env)(i, pp)
      }
    constraints ++ Seq(
      Z3Constraint(
        env.ctx.mkAnd(
          r.<=(env.R.retIdx)(rPlus): _*
        ),
        s"R($pp) <={ret} R(${pp +})",
        pp
      )
    )
  }

  def invokeVirtual(
      i: Instruction35c,
      pp: PP,
      env: Environment,
      addMethodToAnalyze: (DexClass, DexMethod) => Unit): Seq[Z3Constraint] = {
    val mRef = i.getReference.asInstanceOf[MethodReference]
    val r = env.R(pp)
    val rPlus = env.R(pp +)
    val cName = Utils.classPrefix(mRef.toString)
    val mName = Utils.removeClassPrefix(mRef.toString)
    val impls: Map[DexClass, DexClass] =
      env.Dispatch(mName, mRef.getDefiningClass)
    val constraints: Seq[Z3Constraint] =
      if (impls.isEmpty) {
        val returnApprox: Z3Constraint =
          Z3Constraint(
            DexType(mName.substring(mName.indexOf(")") + 1)) match {
              case PrimitiveType() =>
                env.domains.primitiveDomain.approx(
                  env.domains.primitiveDomain.top,
                  rPlus
                    .numbers(env.R.retIdx)
                    .asInstanceOf[env.domains.primitiveDomain.Element]
                )
              case ObjectType(_) =>
                env.domains.referenceDomain.approx(
                  env.domains.referenceDomain.top,
                  rPlus
                    .references(env.R.retIdx)
                    .asInstanceOf[env.domains.referenceDomain.Element]
                )
              case VoidType() =>
                env.ctx.mkTrue()
            },
            s"T <= R(${pp +})(ret)",
            pp
          )
        Seq(returnApprox) ++ taintResultBasedOnParameters(env)(i, pp)
      } else
        impls
          .flatMap(impl => {
            val dexClass = impl._2
            env.preAnalysisResult.objectCreation
              .filter(p => p._2 == impl._1.name)
              .map(objPP => {
                val method = dexClass.methods
                  .find(m => m.name == Utils.removeClassPrefix(mRef.toString))
                  .get
                addMethodToAnalyze(dexClass, method)
                val ppMethod = PP(dexClass, method, 0)
                val thisApprox = Z3Constraint(
                  env.ctx.mkAnd(
                    r.<=(
                      i.getRegisterC,
                      method.registersCount
                    )(env.R(ppMethod)): _*
                  ),
                  s"R($pp)(${i.getRegisterC}) <= R($ppMethod)(${method.registersCount})",
                  pp
                )
                val paramApprox = approxParameters(env)(i, pp, dexClass, method)
                val returnApprox: Z3Constraint =
                  Z3Constraint(
                    env.ctx.mkAnd(
                      env
                        .M(dexClass, method)
                        .map(mResult => {
                          method.returnType match {
                            case PrimitiveType() =>
                              env.domains.primitiveDomain.approx(
                                mResult.asInstanceOf[env.domains.primitiveDomain.Element],
                                rPlus
                                  .numbers(env.R.retIdx)
                              )
                          }
                        })
                        .getOrElse(env.ctx.mkTrue())
                    ),
                    s"M(${dexClass.name},${method.name}) <= R(${pp +})(ret)",
                    pp
                  )
                Z3Constraint(
                  env.ctx.mkImplies(
                    env.ctx.mkAnd(
                      env.domains.referenceDomain.approx(
                        env.domains.referenceDomain.toElement(
                          objPP._1.toString),
                        r.references(i.getRegisterC)
                      )
                    ),
                    env.ctx.mkAnd(
                      env.ctx.mkAnd(
                        paramApprox.map(c => c.boolExpr): _*
                      ),
                      env.ctx.mkAnd(
                        thisApprox.boolExpr
                      ),
                      returnApprox.boolExpr
                    )
                  ),
                  s"${objPP._1.toString} <= R($pp)(${i.getRegisterC}) => " +
                    (if (paramApprox.isEmpty) ""
                     else
                       paramApprox
                         .map(c => c.description)
                         .mkString(" ∧ ") + " ∧ ") + thisApprox.description +
                    " ∧ " + returnApprox.description,
                  pp
                )
              })
          })
          .toList
    var taintConstraints = List[Z3Constraint]()
    if (env.app.sourcesSinks.isSource(cName, mName)) {
      taintConstraints = Z3Constraint(
        env.domains.taintDomain.approx(
          env.domains.taintDomain.top,
          rPlus.taints(env.R.retIdx)
        ),
        s"Taint R(${pp +})(ret)",
        pp
      ) :: taintConstraints
    }
    if (env.app.sourcesSinks.isSink(cName, mName)) {
      taintConstraints = taintConstraints ++ (
        Z3Constraint(
          env.ctx.mkNot(
            env.domains.taintDomain.approx(
              env.domains.taintDomain.top,
              r.taints(i.getRegisterC)
            )
          ),
          s"Check R($pp)(${i.getRegisterC}) LEAK",
          pp
        ) :: checkParametersLeak(env)(i, pp)
      )
    }
    constraints ++ taintConstraints ++ Seq(
      Z3Constraint(
        env.ctx.mkAnd(
          r.<=(env.R.retIdx)(rPlus): _*
        ),
        s"R($pp) <={ret} R(${pp +})",
        pp
      )
    )

  }

  private def classAndSubclasses(env: Environment)(
      definingClass: String): List[DexClass] = {
    env.app.classes.find(c => c.name == definingClass) match {
      case Some(dexClass) => dexClass :: subclasses(env)(dexClass)
      case None           => List()
    }
  }

  private def subclasses(env: Environment)(
      dexClass: DexClass): List[DexClass] = {
    env.app.classes
      .filter(c => c.superClass == dexClass.name)
      .flatMap(c => subclasses(env)(c))
      .toList
  }

}
