/* NSC -- new Scala compiler
 * Copyright 2005-2014 LAMP/EPFL
 * @author  Martin Odersky
 */

package scala.tools.nsc
package backend.jvm
package opt

import scala.annotation.tailrec
import scala.tools.asm
import asm.Opcodes._
import asm.tree._
import scala.collection.convert.decorateAsScala._
import scala.collection.convert.decorateAsJava._
import AsmUtils._
import BytecodeUtils._
import collection.mutable
import scala.tools.asm.tree.analysis.SourceInterpreter
import BackendReporting._
import scala.tools.nsc.backend.jvm.BTypes.InternalName

class Inliner[BT <: BTypes](val btypes: BT) {
  import btypes._
  import callGraph._

  def eliminateUnreachableCodeAndUpdateCallGraph(methodNode: MethodNode, definingClass: InternalName): Unit = {
    localOpt.minimalRemoveUnreachableCode(methodNode, definingClass) foreach {
      case invocation: MethodInsnNode => callGraph.callsites.remove(invocation)
      case _ =>
    }
  }

  def runInliner(): Unit = {
    rewriteFinalTraitMethodInvocations()

    for (request <- collectAndOrderInlineRequests) {
      val Right(callee) = request.callee // collectAndOrderInlineRequests returns callsites with a known callee

      // Inlining a method can create unreachable code. Example:
      //   def f = throw e
      //   def g = f; println() // println is unreachable after inlining f
      // If we have an inline request for a call to g, and f has been already inlined into g, we
      // need to run DCE before inlining g.
      eliminateUnreachableCodeAndUpdateCallGraph(callee.callee, callee.calleeDeclarationClass.internalName)

      // DCE above removes unreachable callsites from the call graph. If the inlining request denotes
      // such an eliminated callsite, do nothing.
      if (callGraph.callsites contains request.callsiteInstruction) {
        val r = inline(request.callsiteInstruction, request.callsiteStackHeight, request.callsiteMethod, request.callsiteClass,
          callee.callee, callee.calleeDeclarationClass,
          request.receiverKnownNotNull, keepLineNumbers = false)

        for (warning <- r) {
          if ((callee.annotatedInline && btypes.compilerSettings.YoptWarningEmitAtInlineFailed) || warning.emitWarning(compilerSettings)) {
            val annotWarn = if (callee.annotatedInline) " is annotated @inline but" else ""
            val msg = s"${BackendReporting.methodSignature(callee.calleeDeclarationClass.internalName, callee.callee)}$annotWarn could not be inlined:\n$warning"
            backendReporting.inlinerWarning(request.callsitePosition, msg)
          }
        }
      }
    }
  }

  /**
   * Ordering for inline requests. Required to make the inliner deterministic:
   *   - Always remove the same request when breaking inlining cycles
   *   - Perform inlinings in a consistent order
   */
  object callsiteOrdering extends Ordering[Callsite] {
    override def compare(x: Callsite, y: Callsite): Int = {
      val cls = x.callsiteClass.internalName compareTo y.callsiteClass.internalName
      if (cls != 0) return cls

      val name = x.callsiteMethod.name compareTo y.callsiteMethod.name
      if (name != 0) return name

      val desc = x.callsiteMethod.desc compareTo y.callsiteMethod.desc
      if (desc != 0) return desc

      def pos(c: Callsite) = c.callsiteMethod.instructions.indexOf(c.callsiteInstruction)
      pos(x) - pos(y)
    }
  }

  /**
   * Select callsites from the call graph that should be inlined. The resulting list of inlining
   * requests is allowed to have cycles, and the callsites can appear in any order.
   */
  def selectCallsitesForInlining: List[Callsite] = {
    callsites.valuesIterator.filter({
      case callsite @ Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, warning)), _, _, _, pos) =>
        val res = doInlineCallsite(callsite)

        if (!res) {
          if (annotatedInline && btypes.compilerSettings.YoptWarningEmitAtInlineFailed) {
            // if the callsite is annotated @inline, we report an inline warning even if the underlying
            // reason is, for example, mixed compilation (which has a separate -Yopt-warning flag).
            def initMsg = s"${BackendReporting.methodSignature(calleeDeclClass.internalName, callee)} is annotated @inline but cannot be inlined"
            def warnMsg = warning.map(" Possible reason:\n" + _).getOrElse("")
            if (doRewriteTraitCallsite(callsite))
              backendReporting.inlinerWarning(pos, s"$initMsg: the trait method call could not be rewritten to the static implementation method." + warnMsg)
            else if (!safeToInline)
              backendReporting.inlinerWarning(pos, s"$initMsg: the method is not final and may be overridden." + warnMsg)
            else
              backendReporting.inlinerWarning(pos, s"$initMsg." + warnMsg)
          } else if (warning.isDefined && warning.get.emitWarning(compilerSettings)) {
            // when annotatedInline is false, and there is some warning, the callsite metadata is possibly incomplete.
            backendReporting.inlinerWarning(pos, s"there was a problem determining if method ${callee.name} can be inlined: \n"+ warning.get)
          }
        }

        res

      case Callsite(ins, _, _, Left(warning), _, _, _, pos) =>
        if (warning.emitWarning(compilerSettings))
          backendReporting.inlinerWarning(pos, s"failed to determine if ${ins.name} should be inlined:\n$warning")
        false
    }).toList
  }

  /**
   * The current inlining heuristics are simple: inline calls to methods annotated @inline.
   */
  def doInlineCallsite(callsite: Callsite): Boolean = callsite match {
    case Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, warning)), _, _, _, pos) =>
      if (compilerSettings.YoptInlineHeuristics.value == "everything") safeToInline
      else annotatedInline && safeToInline

    case _ => false
  }

  def rewriteFinalTraitMethodInvocations(): Unit = {
    // Rewriting final trait method callsites to the implementation class enables inlining.
    // We cannot just iterate over the values of the `callsites` map because the rewrite changes the
    // map. Therefore we first copy the values to a list.
    callsites.values.toList.foreach(rewriteFinalTraitMethodInvocation)
  }

  /**
   * True for statically resolved trait callsites that should be rewritten to the static implementation method.
   */
  def doRewriteTraitCallsite(callsite: Callsite) = callsite.callee match {
    case Right(Callee(callee, calleeDeclarationClass, safeToInline, true, annotatedInline, annotatedNoInline, infoWarning)) => true
    case _ => false
  }

  /**
   * Rewrite the INVOKEINTERFACE callsite of a final trait method invocation to INVOKESTATIC of the
   * corresponding method in the implementation class. This enables inlining final trait methods.
   *
   * In a final trait method callsite, the callee is safeToInline and the callee method is abstract
   * (the receiver type is the interface, so the method is abstract).
   */
  def rewriteFinalTraitMethodInvocation(callsite: Callsite): Unit = {
    if (doRewriteTraitCallsite(callsite)) {
      val Right(Callee(callee, calleeDeclarationClass, _, _, annotatedInline, annotatedNoInline, infoWarning)) = callsite.callee

      val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc)

      val implClassInternalName = calleeDeclarationClass.internalName + "$class"

      val selfParamTypeV: Either[OptimizerWarning, ClassBType] = calleeDeclarationClass.info.map(_.inlineInfo.traitImplClassSelfType match {
        case Some(internalName) => classBTypeFromParsedClassfile(internalName)
        case None               => calleeDeclarationClass
      })

      def implClassMethodV(implMethodDescriptor: String): Either[OptimizerWarning, MethodNode] = {
        byteCodeRepository.methodNode(implClassInternalName, callee.name, implMethodDescriptor).map(_._1)
      }

      // The rewrite reading the implementation class and the implementation method from the bytecode
      // repository. If either of the two fails, the rewrite is not performed.
      val res = for {
        selfParamType        <- selfParamTypeV
        implMethodDescriptor =  asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType.toASMType +: traitMethodArgumentTypes: _*)
        implClassMethod      <- implClassMethodV(implMethodDescriptor)
        implClassBType       =  classBTypeFromParsedClassfile(implClassInternalName)
        selfTypeOk           <- calleeDeclarationClass.isSubtypeOf(selfParamType)
      } yield {

        // The self parameter type may be incompatible with the trait type.
        //   trait T { self: S => def foo = 1 }
        // The $self parameter type of T$class.foo is S, which may be unrelated to T. If we re-write
        // a call to T.foo to T$class.foo, we need to cast the receiver to S, otherwise we get a
        // VerifyError. We run a `SourceInterpreter` to find all producer instructions of the
        // receiver value and add a cast to the self type after each.
        if (!selfTypeOk) {
          // there's no need to run eliminateUnreachableCode here. building the call graph does that
          // already, no code can become unreachable in the meantime.
          val analyzer = new AsmAnalyzer(callsite.callsiteMethod, callsite.callsiteClass.internalName, new SourceInterpreter)
          val receiverValue = analyzer.frameAt(callsite.callsiteInstruction).peekStack(traitMethodArgumentTypes.length)
          for (i <- receiverValue.insns.asScala) {
            val cast = new TypeInsnNode(CHECKCAST, selfParamType.internalName)
            callsite.callsiteMethod.instructions.insert(i, cast)
          }
        }

        val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implMethodDescriptor, false)
        callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction)
        callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction)

        callGraph.callsites.remove(callsite.callsiteInstruction)
        val staticCallsite = Callsite(
          callsiteInstruction = newCallsiteInstruction,
          callsiteMethod      = callsite.callsiteMethod,
          callsiteClass       = callsite.callsiteClass,
          callee              = Right(Callee(
            callee                 = implClassMethod,
            calleeDeclarationClass = implClassBType,
            safeToInline           = true,
            safeToRewrite          = false,
            annotatedInline        = annotatedInline,
            annotatedNoInline      = annotatedNoInline,
            calleeInfoWarning      = infoWarning)),
          argInfos            = Nil,
          callsiteStackHeight = callsite.callsiteStackHeight,
          receiverKnownNotNull = callsite.receiverKnownNotNull,
          callsitePosition = callsite.callsitePosition
        )
        callGraph.callsites(newCallsiteInstruction) = staticCallsite
      }

      for (warning <- res.left) {
        val Right(callee) = callsite.callee
        val newCallee = callee.copy(calleeInfoWarning = Some(RewriteTraitCallToStaticImplMethodFailed(calleeDeclarationClass.internalName, callee.callee.name, callee.callee.desc, warning)))
        callGraph.callsites(callsite.callsiteInstruction) = callsite.copy(callee = Right(newCallee))
      }
    }
  }

  /**
   * Returns the callsites that can be inlined. Ensures that the returned inline request graph does
   * not contain cycles.
   *
   * The resulting list is sorted such that the leaves of the inline request graph are on the left.
   * Once these leaves are inlined, the successive elements will be leaves, etc.
   */
  private def collectAndOrderInlineRequests: List[Callsite] = {
    val requests = selectCallsitesForInlining

    // This map is an index to look up the inlining requests for a method. The value sets are mutable
    // to allow removing elided requests (to break inlining cycles). The map itself is mutable to
    // allow efficient building: requests.groupBy would build values as List[Callsite] that need to
    // be transformed to mutable sets.
    val inlineRequestsForMethod: mutable.Map[MethodNode, mutable.Set[Callsite]] = mutable.HashMap.empty.withDefaultValue(mutable.HashSet.empty)
    for (r <- requests) inlineRequestsForMethod.getOrElseUpdate(r.callsiteMethod, mutable.HashSet.empty) += r

    /**
     * Break cycles in the inline request graph by removing callsites.
     *
     * The list `requests` is traversed left-to-right, removing those callsites that are part of a
     * cycle. Elided callsites are also removed from the `inlineRequestsForMethod` map.
     */
    def breakInlineCycles(requests: List[Callsite]): List[Callsite] = {
      // is there a path of inline requests from start to goal?
      def isReachable(start: MethodNode, goal: MethodNode): Boolean = {
        @tailrec def reachableImpl(check: List[MethodNode], visited: Set[MethodNode]): Boolean = check match {
          case x :: xs =>
            if (x == goal) true
            else if (visited(x)) reachableImpl(xs, visited)
            else {
              val callees = inlineRequestsForMethod(x).map(_.callee.get.callee)
              reachableImpl(xs ::: callees.toList, visited + x)
            }

          case Nil =>
            false
        }
        reachableImpl(List(start), Set.empty)
      }

      val result = new mutable.ListBuffer[Callsite]()
      // sort the inline requests to ensure that removing requests is deterministic
      for (r <- requests.sorted(callsiteOrdering)) {
        // is there a chain of inlining requests that would inline the callsite method into the callee?
        if (isReachable(r.callee.get.callee, r.callsiteMethod))
          inlineRequestsForMethod(r.callsiteMethod) -= r
        else
          result += r
      }
      result.toList
    }

    // sort the remaining inline requests such that the leaves appear first, then those requests
    // that become leaves, etc.
    def leavesFirst(requests: List[Callsite], visited: Set[Callsite] = Set.empty): List[Callsite] = {
      if (requests.isEmpty) Nil
      else {
        val (leaves, others) = requests.partition(r => {
          val inlineRequestsForCallee = inlineRequestsForMethod(r.callee.get.callee)
          inlineRequestsForCallee.forall(visited)
        })
        assert(leaves.nonEmpty, requests)
        leaves ::: leavesFirst(others, visited ++ leaves)
      }
    }

    leavesFirst(breakInlineCycles(requests))
  }


  /**
   * Copy and adapt the instructions of a method to a callsite.
   *
   * Preconditions:
   *   - The maxLocals and maxStack values of the callsite method are correctly computed
   *   - The callsite method contains no unreachable basic blocks, i.e., running an [[Analyzer]]
   *     does not produce any `null` frames
   *
   * @param callsiteInstruction     The invocation instruction
   * @param callsiteStackHeight     The stack height at the callsite
   * @param callsiteMethod          The method in which the invocation occurs
   * @param callsiteClass           The class in which the callsite method is defined
   * @param callee                  The invoked method
   * @param calleeDeclarationClass  The class in which the invoked method is defined
   * @param receiverKnownNotNull    `true` if the receiver is known to be non-null
   * @param keepLineNumbers         `true` if LineNumberNodes should be copied to the call site
   * @return                        `Some(message)` if inlining cannot be performed, `None` otherwise
   */
  def inline(callsiteInstruction: MethodInsnNode, callsiteStackHeight: Int, callsiteMethod: MethodNode, callsiteClass: ClassBType,
             callee: MethodNode, calleeDeclarationClass: ClassBType,
             receiverKnownNotNull: Boolean, keepLineNumbers: Boolean): Option[CannotInlineWarning] = {
    canInline(callsiteInstruction, callsiteStackHeight, callsiteMethod, callsiteClass, callee, calleeDeclarationClass) orElse {
      // New labels for the cloned instructions
      val labelsMap = cloneLabels(callee)
      val (clonedInstructions, instructionMap) = cloneInstructions(callee, labelsMap)
      if (!keepLineNumbers) {
        removeLineNumberNodes(clonedInstructions)
      }

      // local vars in the callee are shifted by the number of locals at the callsite
      val localVarShift = callsiteMethod.maxLocals
      clonedInstructions.iterator.asScala foreach {
        case varInstruction: VarInsnNode => varInstruction.`var` += localVarShift
        case iinc: IincInsnNode          => iinc.`var` += localVarShift
        case _ => ()
      }

      // add a STORE instruction for each expected argument, including for THIS instance if any
      val argStores = new InsnList
      var nextLocalIndex = callsiteMethod.maxLocals
      if (!isStaticMethod(callee)) {
        if (!receiverKnownNotNull) {
          argStores.add(new InsnNode(DUP))
          val nonNullLabel = newLabelNode
          argStores.add(new JumpInsnNode(IFNONNULL, nonNullLabel))
          argStores.add(new InsnNode(ACONST_NULL))
          argStores.add(new InsnNode(ATHROW))
          argStores.add(nonNullLabel)
        }
        argStores.add(new VarInsnNode(ASTORE, nextLocalIndex))
        nextLocalIndex += 1
      }

      // We just use an asm.Type here, no need to create the MethodBType.
      val calleAsmType = asm.Type.getMethodType(callee.desc)

      for(argTp <- calleAsmType.getArgumentTypes) {
        val opc = argTp.getOpcode(ISTORE) // returns the correct xSTORE instruction for argTp
        argStores.insert(new VarInsnNode(opc, nextLocalIndex)) // "insert" is "prepend" - the last argument is on the top of the stack
        nextLocalIndex += argTp.getSize
      }

      clonedInstructions.insert(argStores)

      // label for the exit of the inlined functions. xRETURNs are rplaced by GOTOs to this label.
      val postCallLabel = newLabelNode
      clonedInstructions.add(postCallLabel)

      // replace xRETURNs:
      //   - store the return value (if any)
      //   - clear the stack of the inlined method (insert DROPs)
      //   - load the return value
      //   - GOTO postCallLabel

      val returnType = calleAsmType.getReturnType
      val hasReturnValue = returnType.getSort != asm.Type.VOID
      val returnValueIndex = callsiteMethod.maxLocals + callee.maxLocals
      nextLocalIndex += returnType.getSize

      def returnValueStore(returnInstruction: AbstractInsnNode) = {
        val opc = returnInstruction.getOpcode match {
          case IRETURN => ISTORE
          case LRETURN => LSTORE
          case FRETURN => FSTORE
          case DRETURN => DSTORE
          case ARETURN => ASTORE
        }
        new VarInsnNode(opc, returnValueIndex)
      }

      // We run an interpreter to know the stack height at each xRETURN instruction and the sizes
      // of the values on the stack.
      val analyzer = new AsmAnalyzer(callee, calleeDeclarationClass.internalName)

      for (originalReturn <- callee.instructions.iterator().asScala if isReturn(originalReturn)) {
        val frame = analyzer.frameAt(originalReturn)
        var stackHeight = frame.getStackSize

        val inlinedReturn = instructionMap(originalReturn)
        val returnReplacement = new InsnList

        def drop(slot: Int) = returnReplacement add getPop(frame.peekStack(slot).getSize)

        // for non-void methods, store the stack top into the return local variable
        if (hasReturnValue) {
          returnReplacement add returnValueStore(originalReturn)
          stackHeight -= 1
        }

        // drop the rest of the stack
        for (i <- 0 until stackHeight) drop(i)

        returnReplacement add new JumpInsnNode(GOTO, postCallLabel)
        clonedInstructions.insert(inlinedReturn, returnReplacement)
        clonedInstructions.remove(inlinedReturn)
      }

      // Load instruction for the return value
      if (hasReturnValue) {
        val retVarLoad = {
          val opc = returnType.getOpcode(ILOAD)
          new VarInsnNode(opc, returnValueIndex)
        }
        clonedInstructions.insert(postCallLabel, retVarLoad)
      }

      callsiteMethod.instructions.insert(callsiteInstruction, clonedInstructions)
      callsiteMethod.instructions.remove(callsiteInstruction)

      callsiteMethod.localVariables.addAll(cloneLocalVariableNodes(callee, labelsMap, callee.name + "_").asJava)
      callsiteMethod.tryCatchBlocks.addAll(cloneTryCatchBlockNodes(callee, labelsMap).asJava)

      // Add all invocation instructions that were inlined to the call graph
      callee.instructions.iterator().asScala foreach {
        case originalCallsiteIns: MethodInsnNode =>
          callGraph.callsites.get(originalCallsiteIns) match {
            case Some(originalCallsite) =>
              val newCallsiteIns = instructionMap(originalCallsiteIns).asInstanceOf[MethodInsnNode]
              callGraph.callsites(newCallsiteIns) = Callsite(
                callsiteInstruction = newCallsiteIns,
                callsiteMethod = callsiteMethod,
                callsiteClass = callsiteClass,
                callee = originalCallsite.callee,
                argInfos = Nil, // TODO: re-compute argInfos for new destination (once we actually compute them)
                callsiteStackHeight = callsiteStackHeight + originalCallsite.callsiteStackHeight,
                receiverKnownNotNull = originalCallsite.receiverKnownNotNull,
                callsitePosition = originalCallsite.callsitePosition
              )

            case None =>
          }

        case _ =>
      }
      // Remove the elided invocation from the call graph
      callGraph.callsites.remove(callsiteInstruction)

      // Inlining a method body can render some code unreachable, see example above (in runInliner).
      unreachableCodeEliminated -= callsiteMethod

      callsiteMethod.maxLocals += returnType.getSize + callee.maxLocals
      callsiteMethod.maxStack = math.max(callsiteMethod.maxStack, callee.maxStack + callsiteStackHeight)

      None
    }
  }

  /**
   * Check whether an inling can be performed. Parmeters are described in method [[inline]].
   * @return `Some(message)` if inlining cannot be performed, `None` otherwise
   */
  def canInline(callsiteInstruction: MethodInsnNode, callsiteStackHeight: Int, callsiteMethod: MethodNode, callsiteClass: ClassBType,
                callee: MethodNode, calleeDeclarationClass: ClassBType): Option[CannotInlineWarning] = {

    def calleeDesc = s"${callee.name} of type ${callee.desc} in ${calleeDeclarationClass.internalName}"
    def methodMismatch = s"Wrong method node for inlining ${textify(callsiteInstruction)}: $calleeDesc"
    assert(callsiteInstruction.name == callee.name, methodMismatch)
    assert(callsiteInstruction.desc == callee.desc, methodMismatch)
    assert(!isConstructor(callee), s"Constructors cannot be inlined: $calleeDesc")
    assert(!BytecodeUtils.isAbstractMethod(callee), s"Callee is abstract: $calleeDesc")
    assert(callsiteMethod.instructions.contains(callsiteInstruction), s"Callsite ${textify(callsiteInstruction)} is not an instruction of $calleeDesc")

    // When an exception is thrown, the stack is cleared before jumping to the handler. When
    // inlining a method that catches an exception, all values that were on the stack before the
    // call (in addition to the arguments) would be cleared (SI-6157). So we don't inline methods
    // with handlers in case there are values on the stack.
    // Alternatively, we could save all stack values below the method arguments into locals, but
    // that would be inefficient: we'd need to pop all parameters, save the values, and push the
    // parameters back for the (inlined) invocation. Similarly for the result after the call.
    def stackHasNonParameters: Boolean = {
      val expectedArgs = asm.Type.getArgumentTypes(callsiteInstruction.desc).length + (callsiteInstruction.getOpcode match {
        case INVOKEVIRTUAL | INVOKESPECIAL | INVOKEINTERFACE => 1
        case INVOKESTATIC => 0
        case INVOKEDYNAMIC =>
          assertionError(s"Unexpected opcode, cannot inline ${textify(callsiteInstruction)}")
      })
      callsiteStackHeight > expectedArgs
    }

    if (codeSizeOKForInlining(callsiteMethod, callee)) {
      Some(ResultingMethodTooLarge(
        calleeDeclarationClass.internalName, callee.name, callee.desc,
        callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc))
    } else if (isSynchronizedMethod(callee)) {
      // Could be done by locking on the receiver, wrapping the inlined code in a try and unlocking
      // in finally. But it's probably not worth the effort, scala never emits synchronized methods.
      Some(SynchronizedMethod(calleeDeclarationClass.internalName, callee.name, callee.desc))
    } else if (isStrictfpMethod(callsiteMethod) != isStrictfpMethod(callee)) {
      Some(StrictfpMismatch(
        calleeDeclarationClass.internalName, callee.name, callee.desc,
        callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc))
    } else if (!callee.tryCatchBlocks.isEmpty && stackHasNonParameters) {
      Some(MethodWithHandlerCalledOnNonEmptyStack(
        calleeDeclarationClass.internalName, callee.name, callee.desc,
        callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc))
    } else findIllegalAccess(callee.instructions, calleeDeclarationClass, callsiteClass) map {
      case (illegalAccessIns, None) =>
        IllegalAccessInstruction(
          calleeDeclarationClass.internalName, callee.name, callee.desc,
          callsiteClass.internalName, illegalAccessIns)

      case (illegalAccessIns, Some(warning)) =>
        IllegalAccessCheckFailed(
          calleeDeclarationClass.internalName, callee.name, callee.desc,
          callsiteClass.internalName, illegalAccessIns, warning)
    }
  }

  /**
   * Returns the first instruction in the `instructions` list that would cause a
   * [[java.lang.IllegalAccessError]] when inlined into the `destinationClass`.
   *
   * If validity of some instruction could not be checked because an error occurred, the instruction
   * is returned together with a warning message that describes the problem.
   */
  def findIllegalAccess(instructions: InsnList, calleeDeclarationClass: ClassBType, destinationClass: ClassBType): Option[(AbstractInsnNode, Option[OptimizerWarning])] = {

    /**
     * Check if a type is accessible to some class, as defined in JVMS 5.4.4.
     *  (A1) C is public
     *  (A2) C and D are members of the same run-time package
     */
    def classIsAccessible(accessed: BType, from: ClassBType = destinationClass): Either[OptimizerWarning, Boolean] = (accessed: @unchecked) match {
      // TODO: A2 requires "same run-time package", which seems to be package + classloader (JMVS 5.3.). is the below ok?
      case c: ClassBType     => c.isPublic.map(_ || c.packageInternalName == from.packageInternalName)
      case a: ArrayBType     => classIsAccessible(a.elementType, from)
      case _: PrimitiveBType => Right(true)
    }

    /**
     * Check if a member reference is accessible from the [[destinationClass]], as defined in the
     * JVMS 5.4.4. Note that the class name in a field / method reference is not necessarily the
     * class in which the member is declared:
     *
     *   class A { def f = 0 }; class B extends A { f }
     *
     * The INVOKEVIRTUAL instruction uses a method reference "B.f ()I". Therefore this method has
     * two parameters:
     *
     * @param memberDeclClass The class in which the member is declared (A)
     * @param memberRefClass  The class used in the member reference (B)
     *
     * (B0) JVMS 5.4.3.2 / 5.4.3.3: when resolving a member of class C in D, the class C is resolved
     * first. According to 5.4.3.1, this requires C to be accessible in D.
     *
     * JVMS 5.4.4 summary: A field or method R is accessible to a class D (destinationClass) iff
     *  (B1) R is public
     *  (B2) R is protected, declared in C (memberDeclClass) and D is a subclass of C.
     *       If R is not static, R must contain a symbolic reference to a class T (memberRefClass),
     *       such that T is either a subclass of D, a superclass of D, or D itself.
     *       Also (P) needs to be satisfied.
     *  (B3) R is either protected or has default access and declared by a class in the same
     *       run-time package as D.
     *       If R is protected, also (P) needs to be satisfied.
     *  (B4) R is private and is declared in D.
     *
     *  (P) When accessing a protected instance member, the target object on the stack (the receiver)
     *      has to be a subtype of D (destinationClass). This is enforced by classfile verification
     *      (https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.10.1.8).
     *
     * TODO: we cannot currently implement (P) because we don't have the necessary information
     * available. Once we have a type propagation analysis implemented, we can extract the receiver
     * type from there (https://github.com/scala-opt/scala/issues/13).
     */
    def memberIsAccessible(memberFlags: Int, memberDeclClass: ClassBType, memberRefClass: ClassBType): Either[OptimizerWarning, Boolean] = {
      // TODO: B3 requires "same run-time package", which seems to be package + classloader (JMVS 5.3.). is the below ok?
      def samePackageAsDestination = memberDeclClass.packageInternalName == destinationClass.packageInternalName
      def targetObjectConformsToDestinationClass = false // needs type propagation analysis, see above

      def memberIsAccessibleImpl = {
        val key = (ACC_PUBLIC | ACC_PROTECTED | ACC_PRIVATE) & memberFlags
        key match {
          case ACC_PUBLIC => // B1
            Right(true)

          case ACC_PROTECTED => // B2
            val isStatic = (ACC_STATIC & memberFlags) != 0
            tryEither {
              val condB2 = destinationClass.isSubtypeOf(memberDeclClass).orThrow && {
                isStatic || memberRefClass.isSubtypeOf(destinationClass).orThrow || destinationClass.isSubtypeOf(memberRefClass).orThrow
              }
              Right(
                (condB2 || samePackageAsDestination /* B3 (protected) */) &&
                (isStatic || targetObjectConformsToDestinationClass) // (P)
              )
            }

          case 0 => // B3 (default access)
            Right(samePackageAsDestination)

          case ACC_PRIVATE => // B4
            Right(memberDeclClass == destinationClass)
        }
      }

      classIsAccessible(memberDeclClass) match { // B0
        case Right(true) => memberIsAccessibleImpl
        case r => r
      }
    }

    /**
     * Check if `instruction` can be transplanted to `destinationClass`.
     *
     * If the instruction references a class, method or field that cannot be found in the
     * byteCodeRepository, it is considered as not legal. This is known to happen in mixed
     * compilation: for Java classes there is no classfile that could be parsed, nor does the
     * compiler generate any bytecode.
     *
     * Returns a warning message describing the problem if checking the legality for the instruction
     * failed.
     */
    def isLegal(instruction: AbstractInsnNode): Either[OptimizerWarning, Boolean] = instruction match {
      case ti: TypeInsnNode  =>
        // NEW, ANEWARRAY, CHECKCAST or INSTANCEOF. For these instructions, the reference
        // "must be a symbolic reference to a class, array, or interface type" (JVMS 6), so
        // it can be an internal name, or a full array descriptor.
        classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ti.desc))

      case ma: MultiANewArrayInsnNode =>
        // "a symbolic reference to a class, array, or interface type"
        classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ma.desc))

      case fi: FieldInsnNode =>
        val fieldRefClass = classBTypeFromParsedClassfile(fi.owner)
        for {
          (fieldNode, fieldDeclClassNode) <- byteCodeRepository.fieldNode(fieldRefClass.internalName, fi.name, fi.desc): Either[OptimizerWarning, (FieldNode, InternalName)]
          fieldDeclClass                  =  classBTypeFromParsedClassfile(fieldDeclClassNode)
          res                             <- memberIsAccessible(fieldNode.access, fieldDeclClass, fieldRefClass)
        } yield {
          res
        }

      case mi: MethodInsnNode =>
        if (mi.owner.charAt(0) == '[') Right(true) // array methods are accessible
        else {
          def canInlineCall(opcode: Int, methodFlags: Int, methodDeclClass: ClassBType, methodRefClass: ClassBType): Either[OptimizerWarning, Boolean] = {
            opcode match {
              case INVOKESPECIAL if mi.name != GenBCode.INSTANCE_CONSTRUCTOR_NAME =>
                // invokespecial is used for private method calls, super calls and instance constructor calls.
                // private method and super calls can only be inlined into the same class.
                Right(destinationClass == calleeDeclarationClass)

              case _ => // INVOKEVIRTUAL, INVOKESTATIC, INVOKEINTERFACE and INVOKESPECIAL of constructors
                memberIsAccessible(methodFlags, methodDeclClass, methodRefClass)
            }
          }

          val methodRefClass = classBTypeFromParsedClassfile(mi.owner)
          for {
            (methodNode, methodDeclClassNode) <- byteCodeRepository.methodNode(methodRefClass.internalName, mi.name, mi.desc): Either[OptimizerWarning, (MethodNode, InternalName)]
            methodDeclClass                   =  classBTypeFromParsedClassfile(methodDeclClassNode)
            res                               <- canInlineCall(mi.getOpcode, methodNode.access, methodDeclClass, methodRefClass)
          } yield {
            res
          }
        }

      case ivd: InvokeDynamicInsnNode =>
        // TODO @lry check necessary conditions to inline an indy, instead of giving up
        Right(false)

      case ci: LdcInsnNode => ci.cst match {
        case t: asm.Type => classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(t.getInternalName))
        case _           => Right(true)
      }

      case _ => Right(true)
    }

    val it = instructions.iterator.asScala
    @tailrec def find: Option[(AbstractInsnNode, Option[OptimizerWarning])] = {
      if (!it.hasNext) None // all instructions are legal
      else {
        val i = it.next()
        isLegal(i) match {
          case Left(warning) => Some((i, Some(warning))) // checking isLegal for i failed
          case Right(false)  => Some((i, None))          // an illegal instruction was found
          case _             => find
        }
      }
    }
    find
  }
}
