package scala.reflect.reify

import scala.tools.nsc.Global
import scala.reflect.macros.ReificationException
import scala.reflect.macros.UnexpectedReificationException
import scala.reflect.reify.utils.Utils

/** Given a tree or a type, generate a tree that when executed at runtime produces the original tree or type.
 *  See more info in the comments to `reify` in scala.reflect.api.Universe.
 *
 *  @author   Martin Odersky
 *  @version  2.10
 *  @since    2.10
 */
abstract class Reifier extends States
                          with Phases
                          with Errors
                          with Utils {

  val global: Global
  import global._
  import definitions._
  private val runDefinitions = currentRun.runDefinitions
  import runDefinitions._

  val typer: global.analyzer.Typer
  val universe: Tree
  val mirror: Tree
  val reifee: Any
  val concrete: Boolean

  // needed to seamlessly integrate with standalone utils
  override def getReifier: Reifier { val global: Reifier.this.global.type } =
    this.asInstanceOf[Reifier { val global: Reifier.this.global.type }]
  override def hasReifier = true

  /** For `reifee` and other reification parameters, generate a tree of the form
   *  {{{
   *    {
   *      val \$u: universe.type = <[ universe ]>
   *      val \$m: \$u.Mirror = <[ mirror ]>
   *      \$u.Expr[T](rtree)       // if data is a Tree
   *      \$u.TypeTag[T](rtree)    // if data is a Type
   *    }
   *  }}}
   *
   *  where
   *
   *    - `universe` is the tree that represents the universe the result will be bound to.
   *    - `mirror` is the tree that represents the mirror the result will be initially bound to.
   *    - `rtree` is code that generates `reifee` at runtime.
   *    - `T` is the type that corresponds to `data`.
   *
   *  This is not a method, but a value to indicate the fact that Reifier instances are a one-off.
   */
  lazy val reification: Tree = {
    try {
      if (universe exists (_.isErroneous)) CannotReifyErroneousPrefix(universe)
      if (universe.tpe == null) CannotReifyUntypedPrefix(universe)

      val result = reifee match {
        case tree: Tree =>
          reifyTrace("reifying = ")(if (settings.Xshowtrees || settings.XshowtreesCompact || settings.XshowtreesStringified) "\n" + nodePrinters.nodeToString(tree).trim else tree.toString)
          reifyTrace("reifee is located at: ")(tree.pos)
          reifyTrace("universe = ")(universe)
          reifyTrace("mirror = ")(mirror)
          if (tree exists (_.isErroneous)) CannotReifyErroneousReifee(tree)
          if (tree.tpe == null) CannotReifyUntypedReifee(tree)
          val pipeline = mkReificationPipeline
          val rtree = pipeline(tree)

          val tpe = typer.packedType(tree, NoSymbol)
          val ReifiedType(_, _, tpeSymtab, _, rtpe, tpeReificationIsConcrete) = `package`.reifyType(global)(typer, universe, mirror, tpe, concrete = false)
          state.reificationIsConcrete &= tpeReificationIsConcrete
          state.symtab ++= tpeSymtab
          ReifiedTree(universe, mirror, symtab, rtree, tpe, rtpe, reificationIsConcrete)

        case tpe: Type =>
          reifyTrace("reifying = ")(tpe.toString)
          reifyTrace("universe = ")(universe)
          reifyTrace("mirror = ")(mirror)
          val rtree = reify(tpe)
          ReifiedType(universe, mirror, symtab, tpe, rtree, reificationIsConcrete)

        case _ =>
          throw new Error("reifee %s of type %s is not supported".format(reifee, if (reifee == null) "null" else reifee.getClass.toString))
      }

      // todo. why do we reset attrs?
      //
      // typically we do some preprocessing before reification and
      // the code emitted/moved around during preprocessing is very hard to typecheck, so we leave it as it is
      // however this "as it is" sometimes doesn't make any sense
      //
      // ===example 1===
      // we move a freevar from a nested symbol table to a top-level symbol table,
      // and then the reference to $u becomes screwed up, because nested symbol tables are already typechecked,
      // so we have an $u symbol that points to the nested $u rather than to the top-level one.
      //
      // ===example 2===
      // we inline a freevar by replacing a reference to it, e.g. $u.Apply($u.Select($u.Ident($u.newTermName("$u")), $u.newTermName("Ident")), List($u.Ident($u.newTermName("free$x"))))
      // with its original binding (e.g. $u.Ident("x"))
      // we'd love to typecheck the result, but we cannot do this easily, because $u is external to this tree
      // what's even worse, sometimes $u can point to the top-level symbol table's $u, which doesn't have any symbol/type yet -
      // it's just a ValDef that will be emitted only after the reification is completed
      //
      // hence, the simplest solution is to erase all attrs so that invalid (as well as non-existent) bindings get rebound correctly
      // this is ugly, but it's the best we can do
      //
      // todo. this is a common problem with non-trivial macros in our current macro system
      // needs to be solved some day
      // upd. a new hope: https://groups.google.com/forum/#!topic/scala-internals/TtCTPlj_qcQ
      var importantSymbols = Set[Symbol](
        NothingClass, AnyClass, SingletonClass, PredefModule, ScalaRunTimeModule, TypeCreatorClass, TreeCreatorClass, MirrorClass,
        ApiUniverseClass, JavaUniverseClass, ReflectRuntimePackage, runDefinitions.ReflectRuntimeCurrentMirror)
      importantSymbols ++= importantSymbols map (_.companionSymbol)
      importantSymbols ++= importantSymbols map (_.moduleClass)
      importantSymbols ++= importantSymbols map (_.linkedClassOfClass)
      def isImportantSymbol(sym: Symbol): Boolean = sym != null && sym != NoSymbol && importantSymbols(sym)
      val untyped = brutallyResetAttrs(result, leaveAlone = {
        case ValDef(_, u, _, _) if u == nme.UNIVERSE_SHORT => true
        case ValDef(_, m, _, _) if m == nme.MIRROR_SHORT => true
        case tree if symtab.syms contains tree.symbol => true
        case tree if isImportantSymbol(tree.symbol) => true
        case _ => false
      })

      if (reifyCopypaste) {
        if (reifyDebug) println("=============================")
        println(reifiedNodeToString(untyped))
        if (reifyDebug) println("=============================")
      } else {
        reifyTrace("reification = ")(untyped)
      }

      untyped
    } catch {
      case ex: ReificationException =>
        throw ex
      case ex: UnexpectedReificationException =>
        throw ex
      case ex: Throwable =>
        throw new UnexpectedReificationException(defaultErrorPosition, "reification crashed", ex)
    }
  }
}
