package scala.tools.nsc
package typechecker

import scala.tools.nsc.symtab.Flags

import scala.collection.{mutable, immutable}

/** Duplicate trees and re-type check them, taking care to replace
 *  and create fresh symbols for new local definitions.
 */
abstract class Duplicators extends Analyzer {
  import global._

  def retyped(context: Context, tree: Tree): Tree = {
    resetClassOwners
    (new BodyDuplicator(context)).typed(tree)
  }

  /** Retype the given tree in the given context. Use this method when retyping
   *  a method in a different class. The typer will replace references to the this of
   *  the old class with the new class, and map symbols through the given 'env'. The
   *  environment is a map from type skolems to concrete types (see SpecializedTypes).
   */
  def retyped(context: Context, tree: Tree, oldThis: Symbol, newThis: Symbol, env: collection.Map[Symbol, Type]): Tree = {
    if (oldThis ne newThis) {
      oldClassOwner = oldThis
      newClassOwner = newThis
    } else resetClassOwners

    envSubstitution = new SubstSkolemsTypeMap(env.keysIterator.toList, env.valuesIterator.toList)
    log("retyped with env: " + env)
    (new BodyDuplicator(context)).typed(tree)
  }

  def retypedMethod(context: Context, tree: Tree, oldThis: Symbol, newThis: Symbol): Tree =
    (new BodyDuplicator(context)).retypedMethod(tree.asInstanceOf[DefDef], oldThis, newThis)

  /** Return the special typer for duplicate method bodies. */
  override def newTyper(context: Context): Typer = 
    new BodyDuplicator(context)

  private def resetClassOwners {
    oldClassOwner = null
    newClassOwner = null
  }

  private var oldClassOwner: Symbol = _
  private var newClassOwner: Symbol = _
  private var envSubstitution: SubstTypeMap = _

  private class SubstSkolemsTypeMap(from: List[Symbol], to: List[Type]) extends SubstTypeMap(from, to) {
    protected override def matches(sym1: Symbol, sym2: Symbol) = 
      if (sym2.isTypeSkolem) sym2.deSkolemize eq sym1
      else sym1 eq sym2
  }

  private val invalidSyms: mutable.Map[Symbol, Tree] = mutable.HashMap.empty[Symbol, Tree]
  
  /** A typer that creates new symbols for all definitions in the given tree
   *  and updates references to them while re-typechecking. All types in the 
   *  tree, except for TypeTrees, are erased prior to type checking. TypeTrees
   *  are fixed by substituting invalid symbols for the new ones.
   */
  class BodyDuplicator(context: Context) extends Typer(context: Context) {

    class FixInvalidSyms extends TypeMap {

      def apply(tpe: Type): Type = {
        mapOver(tpe)
      }

      override def mapOver(tpe: Type): Type = tpe match {
        case TypeRef(NoPrefix, sym, args) if sym.isTypeParameterOrSkolem =>
          val sym1 = context.scope.lookup(sym.name)
//          assert(sym1 ne NoSymbol, tpe)
          if ((sym1 ne NoSymbol) && (sym1 ne sym)) {
            log("fixing " + sym + " -> " + sym1)
            typeRef(NoPrefix, sym1, mapOverArgs(args, sym1.typeParams))
          } else super.mapOver(tpe)

        case TypeRef(pre, sym, args) =>
          val newsym = updateSym(sym)
          if (newsym ne sym) {
            log("fixing " + sym + " -> " + newsym)
            typeRef(mapOver(pre), newsym, mapOverArgs(args, newsym.typeParams))
          } else 
            super.mapOver(tpe)
        case _ =>
          super.mapOver(tpe)
      }
    }
    
    /** Fix the given type by replacing invalid symbols with the new ones. */
    def fixType(tpe: Type): Type = {
      val tpe1 = envSubstitution(tpe)
      log("tpe1: " + tpe1)
      (new FixInvalidSyms)(tpe1)
    }

    /** Return the new symbol corresponding to `sym'. */
    private def updateSym(sym: Symbol): Symbol =
      if (invalidSyms.isDefinedAt(sym)) 
        invalidSyms(sym).symbol
      else
        sym
      
    private def invalidate(tree: Tree) {
      if (tree.isDef && tree.symbol != NoSymbol) {
        log("invalid " + tree.symbol)
        invalidSyms(tree.symbol) = tree

        tree match { 
          case ldef @ LabelDef(name, params, rhs) =>        
            log("LabelDef " + name + " sym.info: " + ldef.symbol.info)
            invalidSyms(ldef.symbol) = ldef
          //          breakIf(true, this, ldef, context)
            val newsym = ldef.symbol.cloneSymbol(context.owner)
            newsym.setInfo(fixType(ldef.symbol.info))
            ldef.symbol = newsym
            log("newsym: " + newsym + " info: " + newsym.info)

          case DefDef(_, _, _, _, _, rhs) =>
            // invalidate parameters
            invalidate(tree.asInstanceOf[DefDef].tparams)
            invalidate(List.flatten(tree.asInstanceOf[DefDef].vparamss))
            tree.symbol = NoSymbol

          case _ => 
            tree.symbol = NoSymbol
        }
      }
    }

    private def invalidate(stats: List[Tree]) {
      stats foreach invalidate
    }
      

    def retypedMethod(ddef: DefDef, oldThis: Symbol, newThis: Symbol): Tree = {
      oldClassOwner = oldThis
      newClassOwner = newThis
      invalidate(ddef.tparams)
      for (vdef <- List.flatten(ddef.vparamss)) {
        invalidate(vdef)
        vdef.tpe = null
      }
      ddef.symbol = NoSymbol
      enterSym(context, ddef)
      log("remapping this of " + oldClassOwner + " to " + newClassOwner)
      typed(ddef)
    }
    
    /** Special typer method allowing for re-type checking trees. It expects a typed tree.
     *  Returns a typed tree that has fresh symbols for all definitions in the original tree.
     *
     *  Each definition tree is visited and its symbol added to the invalidSyms map (except LabelDefs),
     *  then cleared (forcing the namer to create fresh symbols).
     *  All invalid symbols found in trees are cleared (except for LabelDefs), forcing the
     *  typechecker to look for fresh ones in the context.
     *
     *  Type trees are typed by substituting old symbols for new ones (@see fixType).
     *
     *  LabelDefs are not typable from trees alone, unless they have the type ()Unit. Therefore,
     *  their symbols are recreated ad-hoc and their types are fixed inline, instead of letting the
     *  namer/typer handle them, or Idents that refer to them.
     */
    override def typed(tree: Tree, mode: Int, pt: Type): Tree = {
      log("typing " + tree)
      if (tree.hasSymbol && tree.symbol != NoSymbol 
          && !tree.symbol.isLabel  // labels cannot be retyped by the type checker as LabelDef has no ValDef/return type trees
          && invalidSyms.isDefinedAt(tree.symbol)) {
        tree.symbol = NoSymbol
      }

      tree match {
        case ttree @ TypeTree() =>
          log("fixing tpe: " + tree.tpe + " with sym: " + tree.tpe.typeSymbol)
          ttree.tpe = fixType(ttree.tpe)
          ttree
        case Block(stats, res) =>
          log("invalidating block")
          invalidate(stats)
          invalidate(res)
          tree.tpe = null
          super.typed(tree, mode, pt)
        
        case ClassDef(_, _, _, tmpl @ Template(parents, _, stats)) =>
//          log("invalidating classdef " + tree.tpe)
          tmpl.symbol = tree.symbol.newLocalDummy(tree.pos)
          invalidate(stats)
          tree.tpe = null
          super.typed(tree, mode, pt)

        case ddef @ DefDef(_, _, _, _, tpt, rhs) =>
          ddef.tpt.tpe = fixType(ddef.tpt.tpe)
          ddef.tpe = null
          super.typed(ddef, mode, pt)
          
        case vdef @ ValDef(_, _, tpt, rhs) =>
//          log("vdef fixing tpe: " + tree.tpe + " with sym: " + tree.tpe.typeSymbol + " and " + invalidSyms)
          vdef.tpt.tpe = fixType(vdef.tpt.tpe)
          vdef.tpe = null
          super.typed(vdef, mode, pt)

        case ldef @ LabelDef(name, params, rhs) =>
          ldef.tpe = null
          val params1 = params map { p => Ident(updateSym(p.symbol)) }
          super.typed(treeCopy.LabelDef(tree, name, params1, rhs), mode, pt)

        case Bind(name, _) =>
          invalidate(tree)
          tree.tpe = null
          super.typed(tree, mode, pt)

        case Ident(_) if tree.symbol.isLabel =>
          log("Ident to labeldef " + tree + " switched to ")
          tree.symbol = updateSym(tree.symbol)
          tree.tpe = null
          super.typed(tree, mode, pt)

        case Select(th @ This(_), sel) if (oldClassOwner ne null) && (th.symbol == oldClassOwner) =>
          log("selection on this, no type ascription required")
          super.typed(atPos(tree.pos)(Select(This(newClassOwner), sel)), mode, pt)

        case This(_) if (oldClassOwner ne null) && (tree.symbol == oldClassOwner) =>
//          val tree1 = Typed(This(newClassOwner), TypeTree(fixType(tree.tpe.widen)))
          val tree1 = This(newClassOwner)
          log("mapped " + tree + " to " + tree1)
          super.typed(atPos(tree.pos)(tree1), mode, pt)

        case Super(qual, mix) if (oldClassOwner ne null) && (tree.symbol == oldClassOwner) =>
          val tree1 = Super(qual, mix)
          log("changed " + tree + " to " + tree1)
          super.typed(atPos(tree.pos)(tree1))

        case _ =>
          tree.tpe = null
          super.typed(tree, mode, pt)
      }
    }
  }
}