package scala.tools.nsc
package typechecker

import symtab.Flags._
import scala.tools.nsc.util._
import scala.reflect.ReflectionUtils

trait Macros { self: Analyzer =>
  import global._
  import definitions._

  def macroMeth(mac: Symbol): Symbol = {
    var owner = mac.owner
    if (!owner.isModuleClass) owner = owner.companionModule.moduleClass
    owner.info.decl(nme.macroMethodName(mac.name))
  }

  def macroArgs(tree: Tree): (List[List[Tree]]) = tree match {
    case Apply(fn, args) =>
      macroArgs(fn) :+ args
    case TypeApply(fn, args) =>
      macroArgs(fn) :+ args
    case Select(qual, name) =>
      List(List(qual))
    case _ =>
      List(List())
  }

  /**
   *  The definition of the method implementing a macro. Example:
   *  Say we have in a class C
   *
   *    def macro foo[T](xs: List[T]): T = expr
   *
   *  Then the following macro method is generated for `foo`:
   *
   *    def defmacro$foo
   *           (_context: scala.reflect.macro.Context)
   *           (_this: _context.Tree)
   *           (T: _context.TypeTree)
   *           (xs: _context.Tree): _context.Tree = {
   *      import _context._  // this means that all methods of Context can be used unqualified in macro's body
   *      expr
   *    }
   *
   *  If macro has no type arguments, the third parameter list is omitted (it's not empty, but omitted altogether).
   *
   *  To find out the desugared representation of your particular macro, compile it with -Ymacro-debug.
   */
  def macroMethDef(mdef: DefDef): Tree = {
    def paramDef(name: Name, tpt: Tree) = ValDef(Modifiers(PARAM), name, tpt, EmptyTree)
    val contextType = TypeTree(ReflectMacroContext.tpe)
    val globParamSec = List(paramDef(nme.macroContext, contextType))
    def globSelect(name: Name) = Select(Ident(nme.macroContext), name)
    def globTree = globSelect(tpnme.Tree)
    def globTypeTree = globSelect(tpnme.TypeTree)
    val thisParamSec = List(paramDef(newTermName(nme.macroThis), globTree))
    def tparamInMacro(tdef: TypeDef) = paramDef(tdef.name.toTermName, globTypeTree)
    def vparamInMacro(vdef: ValDef): ValDef = paramDef(vdef.name, vdef.tpt match {
      case tpt @ AppliedTypeTree(hk, _) if treeInfo.isRepeatedParamType(tpt) => AppliedTypeTree(hk, List(globTree))
      case _ => globTree
    })
    def wrapImplicit(tree: Tree) = atPos(tree.pos) {
      // implicit hasn't proven useful so far, so I'm disabling it
      //val implicitDecl = ValDef(Modifiers(IMPLICIT), nme.macroContextImplicit, SingletonTypeTree(Ident(nme.macroContext)), Ident(nme.macroContext))
      val importGlob = Import(Ident(nme.macroContext), List(ImportSelector(nme.WILDCARD, -1, null, -1)))
      Block(List(importGlob), tree)
    }
    var formals = (mdef.vparamss map (_ map vparamInMacro))
    if (mdef.tparams.nonEmpty) formals = (mdef.tparams map tparamInMacro) :: formals

    atPos(mdef.pos) {
      new DefDef( // can't call DefDef here; need to find out why
        mods = mdef.mods &~ MACRO &~ OVERRIDE,
        name = nme.macroMethodName(mdef.name),
        tparams = List(),
        vparamss = globParamSec :: thisParamSec :: formals,
        tpt = globTree,
        wrapImplicit(mdef.rhs))
    }
  }

  def addMacroMethods(templ: Template, namer: Namer): Unit = {
    for (ddef @ DefDef(mods, _, _, _, _, _) <- templ.body if mods hasFlag MACRO) {
      val trace = scala.tools.nsc.util.trace when settings.Ymacrodebug.value
      val sym = namer.enterSyntheticSym(trace("macro def: ")(macroMethDef(ddef)))
      trace("added to "+namer.context.owner.enclClass+": ")(sym)
    }
  }

  lazy val mirror = new scala.reflect.runtime.Mirror {
    lazy val libraryClassLoader = {
      // todo. this is more or less okay, but not completely correct
      // see https://issues.scala-lang.org/browse/SI-5433 for more info
      val classpath = global.classPath.asURLs
      var loader: java.lang.ClassLoader = ScalaClassLoader.fromURLs(classpath, _root_.java.lang.Object.instancehelper_getClass(self).getClassLoader)

      // an heuristic to detect REPL
      if (global.settings.exposeEmptyPackage.value) {
        import scala.tools.nsc.interpreter._
        val virtualDirectory = global.settings.outputDirs.getSingleOutput.get
        loader = new AbstractFileClassLoader(virtualDirectory, loader) {}
      }

      loader
    }

    override def defaultReflectiveClassLoader() = libraryClassLoader
  }

  /** Return optionally address of companion object and implementation method symbol
   *  of given macro; or None if implementation classfile cannot be loaded or does
   *  not contain the macro implementation.
   */
  def macroImpl(mac: Symbol): Option[(AnyRef, mirror.Symbol)] = {
    val debug = settings.Ymacrodebug.value
    val trace = scala.tools.nsc.util.trace when debug
    trace("looking for macro implementation: ")(mac.fullNameString)

    try {
      val mmeth = macroMeth(mac)
      trace("found implementation at: ")(mmeth.fullNameString)

      if (mmeth == NoSymbol) None
      else {
        trace("loading implementation class: ")(mmeth.owner.fullName)
        trace("classloader is: ")("%s of type %s".format(mirror.libraryClassLoader, _root_.java.lang.Object.instancehelper_getClass(mirror.libraryClassLoader)))
        def inferClasspath(cl: java.lang.ClassLoader) = cl match {
          case cl: java.net.URLClassLoader => "[" + (cl.getURLs mkString ",") + "]"
          case _ => "<unknown>"
        }
        trace("classpath is: ")(inferClasspath(mirror.libraryClassLoader))

        // @xeno.by: relies on the fact that macros can only be defined in static classes
        def classfile(sym: Symbol): String = {
          def recur(sym: Symbol): String = sym match {
            case sym if sym.owner.isPackageClass =>
              val suffix = if (sym.isModuleClass) "$" else ""
              sym.fullName + suffix
            case sym =>
              val separator = if (sym.owner.isModuleClass) "" else "$"
              recur(sym.owner) + separator + sym.javaSimpleName
          }

          if (sym.isClass || sym.isModule) recur(sym)
          else recur(sym.enclClass)
        }

        // @xeno.by: this doesn't work for inner classes
        // neither does mmeth.owner.javaClassName, so I had to roll my own implementation
        //val receiverName = mmeth.owner.fullName
        val receiverName = classfile(mmeth.owner)
        val receiverClass: mirror.Symbol = mirror.symbolForName(receiverName)

        if (debug) {
          println("receiverClass is: " + receiverClass.fullNameString)

          val jreceiverClass = mirror.classToJava(receiverClass)
          val jreceiverSource = jreceiverClass.getProtectionDomain.getCodeSource
          println("jreceiverClass is %s from %s".format(jreceiverClass, jreceiverSource))
          println("jreceiverClassLoader is %s with classpath %s".format(jreceiverClass.getClassLoader, inferClasspath(jreceiverClass.getClassLoader)))
        }

        val receiverObj = receiverClass.companionModule
        trace("receiverObj is: ")(receiverObj.fullNameString)

        if (receiverObj == mirror.NoSymbol) None
        else {
          // @xeno.by: yet another reflection method that doesn't work for inner classes
          //val receiver = mirror.companionInstance(receiverClass)
          val clazz = java.lang.Class.forName(receiverName, true, mirror.libraryClassLoader)
          val receiver = clazz getField "MODULE$" get null

          val rmeth = receiverObj.info.member(mirror.newTermName(mmeth.name.ToString))
          if (debug) {
            println("rmeth is: " + rmeth.fullNameString)
            println("jrmeth is: " + mirror.methodToJava(rmeth))
          }

          if (rmeth == mirror.NoSymbol) None
          else {
            Some((receiver, rmeth))
          }
        }
      }
    } catch {
      case ex: java.lang.ClassNotFoundException =>
        trace("implementation class failed to load: ")(ex.ToString)
        None
    }
  }

  /** Return result of macro expansion.
   *  Or, if that fails, and the macro overrides a method return
   *  tree that calls this method instead of the macro.
   */
  def macroExpand(tree: Tree, typer: Typer): Option[Any] = {
    val trace = scala.tools.nsc.util.trace when settings.Ymacrodebug.value
    trace("macroExpand: ")(tree)

    val macroDef = tree.symbol
    macroImpl(macroDef) match {
      case Some((receiver, rmeth)) =>
        val argss = List(global) :: macroArgs(tree)
        val paramss = macroMeth(macroDef).paramss
        trace("paramss: ")(paramss)
        val rawArgss = for ((as, ps) <- argss zip paramss) yield {
          if (isVarArgsList(ps)) as.take(ps.length - 1) :+ as.drop(ps.length - 1)
          else as
        }
        val rawArgs: Seq[Any] = rawArgss.flatten
        trace("rawArgs: ")(rawArgs)
        val savedInfolevel = nodePrinters.infolevel
        try {
          // @xeno.by: InfoLevel.Verbose examines and prints out infos of symbols
          // by the means of this'es these symbols can climb up the lexical scope
          // when these symbols will be examined by a node printer
          // they will enumerate and analyze their children (ask for infos and tpes)
          // if one of those children involves macro expansion, things might get nasty
          // that's why I'm temporarily turning this behavior off
          nodePrinters.infolevel = nodePrinters.InfoLevel.Quiet
          val expanded = mirror.invoke(receiver, rmeth)(rawArgs: _*)
          expanded match {
            case expanded: Tree =>
              val expectedTpe = tree.tpe
              val typed = typer.typed(expanded, EXPRmode, expectedTpe)
              Some(typed)
            case expanded if expanded.isInstanceOf[Tree] =>
              typer.context.unit.error(tree.pos, "macro must return a compiler-specific tree; returned value is Tree, but it doesn't belong to this compiler's universe")
              None
            case expanded =>
              typer.context.unit.error(tree.pos, "macro must return a compiler-specific tree; returned value is of class: " + expanded.GetType)
              None
          }
        } catch {
          case ex =>
            val realex = ReflectionUtils.unwrapThrowable(ex)
            val msg = if (settings.Ymacrodebug.value) {
              val stacktrace = new java.io.StringWriter()
              java.lang.Throwable.instancehelper_printStackTrace(realex, new java.io.PrintWriter(stacktrace))
              java.lang.System.getProperty("line.separator") + stacktrace
            } else {
              realex.Message
            }
            typer.context.unit.error(tree.pos, "exception during macro expansion: " + msg)
            None
        } finally {
          nodePrinters.infolevel = savedInfolevel
        }
      case None =>
        def notFound() = {
          typer.context.unit.error(tree.pos, "macro implementation not found: " + macroDef.name)
          None
        }
        def fallBackToOverridden(tree: Tree): Option[Tree] = {
          tree match {
            case Select(qual, name) if (macroDef.isMacro) =>
              macroDef.allOverriddenSymbols match {
                case first :: _ =>
                  Some(Select(qual, name) setPos tree.pos setSymbol first)
                case _ =>
                  trace("macro is not overridden: ")(tree)
                  notFound()
              }
            case Apply(fn, args) =>
              fallBackToOverridden(fn) match {
                case Some(fn1) => Some(Apply(fn1, args) setPos tree.pos)
                case _         => None
              }
            case TypeApply(fn, args) =>
              fallBackToOverridden(fn) match {
                case Some(fn1) => Some(TypeApply(fn1, args) setPos tree.pos)
                case _         => None
              }
            case _ =>
              trace("unexpected tree in fallback: ")(tree)
              notFound()
          }
        }
        fallBackToOverridden(tree) match {
          case Some(tree1) =>
            trace("falling back to ")(tree1)
            currentRun.macroExpansionFailed = true
            Some(tree1)
          case None =>
            None
        }
    }
  }
}
