package scala.tools.nsc
package dependencies;
import util.SourceFile;
import io.AbstractFile
import collection._
import symtab.Flags

trait DependencyAnalysis extends SubComponent with Files {
  import global._

  val phaseName = "dependencyAnalysis";

  def off = settings.make.value == "all"

  def newPhase(prev : Phase) = new AnalysisPhase(prev) 

  lazy val maxDepth = settings.make.value match {
    case "changed" => 0 
    case "transitive" => Int.MaxValue
    case "immediate" => 1 
  }

  def nameToFile(src: AbstractFile, name : String) = 
    settings.outputDirs.outputDirFor(src)
      .lookupPathUnchecked(name.toString.replace(".", java.io.File.separator) + ".class", false)

  private var depFile: Option[AbstractFile] = None

  def dependenciesFile_=(file: AbstractFile) {
    assert(file ne null)
    depFile = Some(file)
  }

  def dependenciesFile: Option[AbstractFile] = depFile

  def classpath = settings.classpath.value
  def newDeps = new FileDependencies(classpath);

  var dependencies = newDeps

  def managedFiles = dependencies.dependencies.keySet
  
  /** Top level definitions per source file. */
  val definitions: mutable.Map[AbstractFile, List[Symbol]] =
    new mutable.HashMap[AbstractFile, List[Symbol]] {
      override def default(f : AbstractFile) = Nil
  }
  
  /** External references used by source file. */
  val references: mutable.Map[AbstractFile, immutable.Set[String]] = 
    new mutable.HashMap[AbstractFile, immutable.Set[String]] {
      override def default(f : AbstractFile) = immutable.Set()
    }

  /** Write dependencies to the current file. */
  def saveDependencies(fromFile: AbstractFile => String) = 
    if(dependenciesFile.isDefined)
      dependencies.writeTo(dependenciesFile.get, fromFile)

  /** Load dependencies from the given file and save the file reference for
   *  future saves.
   */
  def loadFrom(f: AbstractFile, toFile: String => AbstractFile) : Boolean = {
    dependenciesFile = f
    FileDependencies.readFrom(f, toFile) match {
      case Some(fd) =>      
        val success = fd.classpath == classpath
        dependencies = if (success) fd else {
          if(settings.debug.value){
            println("Classpath has changed. Nuking dependencies");
          }
          newDeps
        }
        
        success
      case None => false
    }
  }

  def filter(files : List[SourceFile]) : List[SourceFile] = 
    if (off) files
    else if (dependencies.isEmpty){
      if(settings.debug.value){
        println("No known dependencies. Compiling everything");
      }
      files
    }
    else {
      val (direct, indirect) = dependencies.invalidatedFiles(maxDepth);  
      val filtered = files.filter(x => {
        val f = x.file.absolute
        direct(f) || indirect(f) || !dependencies.containsFile(f);
      })
      filtered match {
        case Nil => println("No changes to recompile");
        case x => println("Recompiling " + (
          if(settings.debug.value) x.mkString(", ")
          else x.length + " files")
        )
      }
      filtered
    }

  class AnalysisPhase(prev : Phase) extends StdPhase(prev){
    def apply(unit : global.CompilationUnit) { 
      val f = unit.source.file.file;
      // When we're passed strings by the interpreter 
      // they  have no source file. We simply ignore this case
      // as irrelevant to dependency analysis.
      if (f != null){
        val source: AbstractFile = unit.source.file;
        for (d <- unit.icode){
          val name = d.symbol match {
            case _ : ModuleClassSymbol => d.toString+"$"
            case _ => d.toString
          }
          dependencies.emits(source, nameToFile(unit.source.file, name))
        }
       
        for (d <- unit.depends; if (d.sourceFile != null)){
          dependencies.depends(source, d.sourceFile);
        }
      }

      // find all external references in this compilation unit
      val file = unit.source.file
      references += file -> immutable.Set.empty[String]

      val buf = new mutable.ListBuffer[Symbol]

      (new Traverser {
        override def traverse(tree: Tree) {
          if ((tree.symbol ne null)
              && (tree.symbol != NoSymbol)
              && (!tree.symbol.isPackage)
              && (!tree.symbol.hasFlag(Flags.JAVA))
              && ((tree.symbol.sourceFile eq null)
                  || (tree.symbol.sourceFile.path != file.path))) {
            references += file -> (references(file) + tree.symbol.fullNameString)
          }
          tree match {
            case cdef: ClassDef if !cdef.symbol.isModuleClass && !cdef.symbol.hasFlag(Flags.PACKAGE) =>
              buf += cdef.symbol
              super.traverse(tree)

            case _ =>
              super.traverse(tree)
          }
        }
      }).apply(unit.body)

      definitions(unit.source.file) = buf.toList
    }
  }
}