package scala.tools.nsc
package interactive

import scala.collection._
import scala.tools.nsc.reporters.{Reporter, ConsoleReporter}
import scala.util.control.Breaks._

import dependencies._
import util.FakePos
import io.AbstractFile

/** A more defined build manager, based on change sets. For each
 *  updated source file, it computes the set of changes to its
 *  definitions, then checks all dependent units to see if the
 *  changes require a compilation. It repeats this process until
 *  a fixpoint is reached.
 */
class RefinedBuildManager(val settings: Settings) extends Changes with BuildManager {

  class BuilderGlobal(settings: Settings) extends scala.tools.nsc.Global(settings)  {

    override def computeInternalPhases() {
      super.computeInternalPhases
      phasesSet += dependencyAnalysis
    }
    
    def newRun() = new Run()
  }
  
  protected def newCompiler(settings: Settings) = new BuilderGlobal(settings) 
  
  val compiler = newCompiler(settings)
  import compiler.Symbol

  /** Managed source files. */
  private val sources: mutable.Set[AbstractFile] = new mutable.HashSet[AbstractFile]

  private val definitions: mutable.Map[AbstractFile, List[Symbol]] = 
    new mutable.HashMap[AbstractFile, List[Symbol]] {
      override def default(key: AbstractFile) = Nil
    }

  /** External references used by source file. */
  private var references: mutable.Map[AbstractFile, immutable.Set[String]] = _

  /** Add the given source files to the managed build process. */
  def addSourceFiles(files: Set[AbstractFile]) {
    sources ++= files
    update(files)
  }

  /** Remove the given files from the managed build process. */
  def removeFiles(files: Set[AbstractFile]) {
    sources --= files
    update(invalidatedByRemove(files))
  }

  /** Return the set of invalidated files caused by removing the given files.
   */
  private def invalidatedByRemove(files: Set[AbstractFile]): Set[AbstractFile] = {
    val changes = new mutable.HashMap[Symbol, List[Change]]
    for (f <- files; sym <- definitions(f))
      changes += sym -> List(Removed(Class(sym.fullNameString)))
    invalidated(files, changes)
  }

  def update(added: Set[AbstractFile], removed: Set[AbstractFile]) {
    sources --= removed
    update(added ++ invalidatedByRemove(removed))
  }

  /** The given files have been modified by the user. Recompile
   *  them and all files that depend on them. Only files that
   *  have been previously added as source files are recompiled.
   */
  private def update(files: Set[AbstractFile]): Unit = if (!files.isEmpty) {
    val run = compiler.newRun()
    compiler.inform("compiling " + files)
    buildingFiles(files)

    run.compileFiles(files.toList)
    if (compiler.reporter.hasErrors) {
      compiler.reporter.reset
      return
    }

    val changesOf = new mutable.HashMap[Symbol, List[Change]]

    val defs = compiler.dependencyAnalysis.definitions
    for (val src <- files; val syms = defs(src); val sym <- syms) {
      definitions(src).find(_.fullNameString == sym.fullNameString) match {
        case Some(oldSym) => 
          changesOf(oldSym) = changeSet(oldSym, sym)
        case _ =>
          // a new top level definition, no need to process
      }
    }
    println("Changes: " + changesOf)
    updateDefinitions(files)
    update(invalidated(files, changesOf))
  }

  /** Return the set of source files that are invalidated by the given changes. */
  def invalidated(files: Set[AbstractFile], changesOf: collection.Map[Symbol, List[Change]]): Set[AbstractFile] = {
    val buf = new mutable.HashSet[AbstractFile]    
    var directDeps = 
      compiler.dependencyAnalysis.dependencies.dependentFiles(1, files)

//    println("direct dependencies on " + files + " " + directDeps)
    def invalidate(file: AbstractFile, reason: String, change: Change) = {
      println("invalidate " + file + " because " + reason + " [" + change + "]")
      buf += file
      directDeps -= file
      break
    }
    
    // changesOf will be empty just after initialization with a saved
    // dependencies file.
    if (changesOf.isEmpty)
      buf ++= directDeps
    else { 
      for ((oldSym, changes) <- changesOf; change <- changes) {
  
        def checkParents(cls: Symbol, file: AbstractFile) {
          val parentChange = cls.info.parents.exists(_.typeSymbol.fullNameString == oldSym.fullNameString)
//          println("checkParents " + cls + " oldSym: " + oldSym + " parentChange: " + parentChange + " " + cls.info.parents)
          change match {
            case Changed(Class(_)) if parentChange =>
              invalidate(file, "parents have changed", change)
          
            case Added(Definition(_)) if parentChange =>
              invalidate(file, "inherited new method", change)
  
            case Removed(Definition(_)) if parentChange =>
              invalidate(file, "inherited method removed", change)
            
            case _ => ()
          }
        }
  
        def checkInterface(cls: Symbol, file: AbstractFile) {
          change match {
            case Added(Definition(name)) =>
              if (cls.info.decls.iterator.exists(_.fullNameString == name))
                invalidate(file, "of new method with existing name", change)
            case Changed(Class(name)) =>
              if (cls.info.typeSymbol.fullNameString == name)
                invalidate(file, "self type changed", change)
            case _ =>
              ()
          }
        }
  
        def checkReferences(file: AbstractFile) {
//          println(file + ":" + references(file))
          val refs = references(file)
          if (refs.isEmpty)
            invalidate(file, "it is a direct dependency and we don't yet have finer-grained dependency information", change)
          else {
            change match {
              case Removed(Definition(name)) if refs(name) =>
                invalidate(file, "it references deleted definition", change)
              case Removed(Class(name)) if (refs(name)) =>
                invalidate(file, "it references deleted class", change)
              case Changed(Definition(name)) if (refs(name)) =>
                invalidate(file, "it references changed definition", change)
              case _ => ()
            }
          }
        }
  
        breakable {
          for (file <- directDeps) {
            for (cls <- definitions(file)) checkParents(cls, file)
            for (cls <- definitions(file)) checkInterface(cls, file)
            checkReferences(file)
          }
        }
      }
    }
    
    buf
  }

  /** Update the map of definitions per source file */
  private def updateDefinitions(files: Set[AbstractFile]) {
    for (src <- files; val localDefs = compiler.dependencyAnalysis.definitions(src)) {
      definitions(src) = (localDefs map (_.cloneSymbol))
    }
    this.references = compiler.dependencyAnalysis.references
  }

  /** Load saved dependency information. */
  def loadFrom(file: AbstractFile, toFile: String => AbstractFile) : Boolean = {
    val success = compiler.dependencyAnalysis.loadFrom(file, toFile)
    if (success)
      sources ++= compiler.dependencyAnalysis.managedFiles
    success
  }
  
  /** Save dependency information to `file'. */
  def saveTo(file: AbstractFile, fromFile: AbstractFile => String) {
    compiler.dependencyAnalysis.dependenciesFile = file
    compiler.dependencyAnalysis.saveDependencies(fromFile)
  }
}