package scala.tools.nsc
package dependencies

import java.io.{InputStream, OutputStream, PrintStream, InputStreamReader, BufferedReader}
import io.{AbstractFile, PlainFile, VirtualFile}

import scala.collection._


trait Files { self : SubComponent =>

  class FileDependencies(val classpath: String) {
    import FileDependencies._

    class Tracker extends mutable.OpenHashMap[AbstractFile, mutable.Set[AbstractFile]] {
      override def default(key: AbstractFile) = {
        this(key) = new mutable.HashSet[AbstractFile]
        this(key)
      }
    }

    val dependencies = new Tracker
    val targets =  new Tracker

    def isEmpty = dependencies.isEmpty && targets.isEmpty

    def emits(source: AbstractFile, result: AbstractFile) = 
      targets(source) += result
    def depends(from: AbstractFile, on: AbstractFile) = 
      dependencies(from) += on

    def reset(file: AbstractFile) = dependencies -= file
    
    def cleanEmpty = {
      dependencies foreach {case (_, value) =>
                               value retain (x => x.exists && (x ne removedFile))}
      dependencies retain ((key, value) => key.exists && !value.isEmpty)
      targets foreach {case (_, value) => value retain (_.exists)}
      targets retain ((key, value) => key.exists && !value.isEmpty)
    }

    def containsFile(f: AbstractFile) = targets.contains(f.absolute)        

    def invalidatedFiles(maxDepth: Int) = {
      val direct = new mutable.HashSet[AbstractFile]

      for ((file, products) <- targets) {
        // This looks a bit odd. It may seem like one should invalidate a file
        // if *any* of its dependencies are older than it. The forall is there
        // to deal with the fact that a) Some results might have been orphaned
        // and b) Some files might not need changing. 
        direct(file) ||= products.forall(d => d.lastModified < file.lastModified)
      }

      val indirect = dependentFiles(maxDepth, direct)

      for ((source, targets) <- targets  
           if direct(source) || indirect(source) || (source eq removedFile)) {
        targets foreach (_.delete)
        targets -= source
      }

      (direct, indirect)
    }

    /** Return the set of files that depend on the given changed files.
     *  It computes the transitive closure up to the given depth.
     */
    def dependentFiles(depth: Int, changed: Set[AbstractFile]): Set[AbstractFile] = {
      val indirect = new mutable.HashSet[AbstractFile]
      val newInvalidations = new mutable.HashSet[AbstractFile]

      def invalid(file: AbstractFile) =
        indirect(file) || changed(file) || (file eq removedFile)

      def go(i: Int) : Unit = if(i > 0) {
        newInvalidations.clear
        for((target, depends) <- dependencies if !invalid(target);
            d <- depends)
          newInvalidations(target) ||= invalid(d)

        indirect ++= newInvalidations
        if (!newInvalidations.isEmpty) go(i - 1)
      }

      go(depth)

      indirect --= changed
    }

    def writeTo(file: AbstractFile, fromFile: AbstractFile => String): Unit = 
      writeToFile(file)(out => writeTo(new PrintStream(out), fromFile))
    
    def writeTo(print: PrintStream, fromFile: AbstractFile => String): Unit = {
      def emit(tracker: Tracker) =
        for ((f, ds) <- tracker; d <- ds) print.println(fromFile(f) + arrow + fromFile(d))
      
      cleanEmpty
      print.println(classpath)
      print.println(separator)
      emit(dependencies)
      print.println(separator)
      emit(targets)
    }
  }

  object FileDependencies {
    private val separator:String = "-------"
    private val arrow = " -> "
    private val removedFile = new VirtualFile("removed")
    
    private def validLine(l: String) = (l != null) && (l != separator)

    def readFrom(file: AbstractFile, toFile: String => AbstractFile): Option[FileDependencies] =
      readFromFile(file) { in =>
        val reader = new BufferedReader(new InputStreamReader(in))
        val it = new FileDependencies(reader.readLine)
        
        def readLines(valid: Boolean)(f: (AbstractFile, AbstractFile) => Unit): Boolean = {
          var continue = valid
          var line: String = null
          while (continue && {line = reader.readLine; validLine(line)}) { 
            line.split(arrow) match {
              case Array(from, on) => f(toFile(from), toFile(on))
              case _ =>
                global.inform("Parse error: Unrecognised string " + line)
                continue = false
            }
          }
          continue
        }
        
        reader.readLine

        val dResult = readLines(true)(
          (_, _) match {
            case (null, _)          => // fromFile is removed, it's ok
            case (fromFile, null)   =>
              // onFile is removed, should recompile fromFile
              it.depends(fromFile, removedFile) 
            case (fromFile, onFile) => it.depends(fromFile, onFile)
          })
        
        readLines(dResult)(
          (_, _) match {
            case (null, null)             =>
              // source and target are all removed, it's ok
            case (null, targetFile)       =>
              // source is removed, should remove relative target later
              it.emits(removedFile, targetFile)
            case (_, null)                =>
              // it may has been cleaned outside, or removed during last phase
            case (sourceFile, targetFile) => it.emits(sourceFile, targetFile)
          })
          
        Some(it)
      }
  }

  def writeToFile[T](file: AbstractFile)(f: OutputStream => T) : T = {
    val out = file.bufferedOutput
    try {
      f(out) 
    } finally {
      out.close
    }
  }

  def readFromFile[T](file: AbstractFile)(f: InputStream => T) : T = {
    val in = file.input
    try{
      f(in)
    } finally {
      in.close
    }
  }
}
