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

package scala.tools.nsc
package transform

import symtab._
import Flags._
import scala.collection.{ mutable, immutable }
import scala.collection.mutable.ListBuffer

abstract class Flatten extends InfoTransform {
  import global._
  import definitions._

  /** the following two members override abstract members in Transform */
  val phaseName: String = "flatten"

  /** Updates the owning scope with the given symbol; returns the old symbol.
   */
  private def replaceSymbolInCurrentScope(sym: Symbol): Symbol = afterFlatten {
    val scope = sym.owner.info.decls
    val old   = scope lookup sym.name andAlso scope.unlink
    scope enter sym

    if (old eq NoSymbol)
      log(s"lifted ${sym.fullLocationString}")
    else
      log(s"lifted ${sym.fullLocationString} after unlinking existing $old from scope.")

    old
  }

  private def liftClass(sym: Symbol) {
    if (!sym.isLifted) {
      sym setFlag LIFTED
      debuglog("re-enter " + sym.fullLocationString)
      replaceSymbolInCurrentScope(sym)
    }
  }
  private def liftSymbol(sym: Symbol) {
    liftClass(sym)
    if (sym.needsImplClass)
      liftClass(erasure implClass sym)
  }
  // This is a short-term measure partially working around objects being
  // lifted out of parameterized classes, leaving them referencing
  // invisible type parameters.
  private def isFlattenablePrefix(pre: Type) = {
    val clazz = pre.typeSymbol
    clazz.isClass && !clazz.isPackageClass && {
      // Cannot flatten here: class A[T] { object B }
      // was "at erasurePhase.prev"
      beforeErasure(clazz.typeParams.isEmpty)
    }
  }

  private val flattened = new TypeMap {
    def apply(tp: Type): Type = tp match {
      case TypeRef(pre, sym, args) if isFlattenablePrefix(pre) =>
        assert(args.isEmpty && sym.enclosingTopLevelClass != NoSymbol, sym.ownerChain)
        typeRef(sym.enclosingTopLevelClass.owner.thisType, sym, Nil)
      case ClassInfoType(parents, decls, clazz) =>
        var parents1 = parents
        val decls1 = scopeTransform(clazz) {
          val decls1 = newScope
          if (clazz.isPackageClass) {
            afterFlatten { decls foreach (decls1 enter _) }
          }
          else {
            val oldowner = clazz.owner
            afterFlatten { oldowner.info }
            parents1 = parents mapConserve (this)

            for (sym <- decls) {
              if (sym.isTerm && !sym.isStaticModule) {
                decls1 enter sym
                if (sym.isModule)
                  sym.moduleClass setFlag LIFTED
              } else if (sym.isClass)
                liftSymbol(sym)
            }
          }
          decls1
        }
        ClassInfoType(parents1, decls1, clazz)
      case MethodType(params, restp) =>
        val restp1 = apply(restp)
        if (restp1 eq restp) tp else copyMethodType(tp, params, restp1)
      case PolyType(tparams, restp) =>
        val restp1 = apply(restp);
        if (restp1 eq restp) tp else PolyType(tparams, restp1)
      case _ =>
        mapOver(tp)
    }
  }

  def transformInfo(sym: Symbol, tp: Type): Type = flattened(tp)

  protected def newTransformer(unit: CompilationUnit): Transformer = new Flattener

  class Flattener extends Transformer {
    /** Buffers for lifted out classes */
    private val liftedDefs = perRunCaches.newMap[Symbol, ListBuffer[Tree]]()

    override def transform(tree: Tree): Tree = {
      tree match {
        case PackageDef(_, _) =>
          liftedDefs(tree.symbol.moduleClass) = new ListBuffer
        case Template(_, _, _) if tree.symbol.isDefinedInPackage =>
          liftedDefs(tree.symbol.owner) = new ListBuffer
        case _ =>
      }
      postTransform(super.transform(tree))
    }

    private def postTransform(tree: Tree): Tree = {
      val sym = tree.symbol
      val tree1 = tree match {
        case ClassDef(_, _, _, _) if sym.isNestedClass =>
          liftedDefs(sym.enclosingTopLevelClass.owner) += tree
          EmptyTree
        case Select(qual, name) if (sym.isStaticModule && !sym.owner.isPackageClass) =>
          afterFlatten(atPos(tree.pos)(gen.mkAttributedRef(sym)))
        case _ =>
          tree
      }
      tree1 setType flattened(tree1.tpe)
    }

    /** Transform statements and add lifted definitions to them. */
    override def transformStats(stats: List[Tree], exprOwner: Symbol): List[Tree] = {
      val stats1 = super.transformStats(stats, exprOwner)
      if (currentOwner.isPackageClass) stats1 ::: liftedDefs(currentOwner).toList
      else stats1
    }
  }
}
