/* sbt -- Simple Build Tool
 * Copyright 2010  Mark Harrah
 */
package sbt

	import java.io.File

/** 
Maintains a set of mappings so that they are uptodate.
Specifically, 'apply' applies the mappings by creating target directories and copying source files to their destination.
For each mapping no longer present, the old target is removed.
Caution: Existing files are overwritten.
Caution: The removal of old targets assumes that nothing else has written to or modified those files.
  It tries not to obliterate large amounts of data by only removing previously tracked files and empty directories.
  That is, it won't remove a directory with unknown (untracked) files in it.
Warning: It is therefore inappropriate to use this with anything other than an automatically managed destination or a dedicated target directory.
Warning: Specifically, don't mix this with a directory containing manually created files, like sources.
It is safe to use for its intended purpose: copying resources to a class output directory.
*/
object Sync
{
	def apply(cacheFile: File, inStyle: FileInfo.Style = FileInfo.lastModified, outStyle: FileInfo.Style = FileInfo.exists): Traversable[(File,File)] => Relation[File,File] =
		mappings =>
		{
			val relation = Relation.empty ++ mappings
			noDuplicateTargets(relation)
			val currentInfo = relation._1s.map(s => (s, inStyle(s)) ).toMap

			val (previousRelation, previousInfo) = readInfo(cacheFile)(inStyle.format)
			val removeTargets = previousRelation._2s -- relation._2s

			def outofdate(source: File, target: File): Boolean =
				!previousRelation.contains(source, target) ||
				(previousInfo get source) != (currentInfo get source) ||
				!target.exists ||
				target.isDirectory != source.isDirectory

			val updates = relation filter outofdate

			val (cleanDirs, cleanFiles) = (updates._2s ++ removeTargets).partition(_.isDirectory)

			IO.delete(cleanFiles)
			IO.deleteIfEmpty(cleanDirs)
			updates.all.foreach((copy _).tupled)

			writeInfo(cacheFile, relation, currentInfo)(inStyle.format)
			relation
		}
		
	def copy(source: File, target: File): Unit =
		if(source.isFile)
			IO.copyFile(source, target, true)
		else if(!target.exists) // we don't want to update the last modified time of an existing directory
		{
			IO.createDirectory(target)
			IO.copyLastModified(source, target)
		}

	def noDuplicateTargets(relation: Relation[File, File])
	{
		val dups = relation.reverseMap.filter { case (target, srcs) =>
			srcs.size >= 2 && srcs.exists(!_.isDirectory)
		} map { case (target, srcs) =>
			"\n\t" + target + "\nfrom\n\t" + srcs.mkString("\n\t\t")
		}
		if(!dups.isEmpty)
			sys.error("Duplicate mappings:" + dups.mkString)
	}
		
		import java.io.{File, IOException}
		import sbinary._
		import Operations.{read, write}
		import DefaultProtocol.{FileFormat => _, _}
		import inc.AnalysisFormats._

	def writeInfo[F <: FileInfo](file: File, relation: Relation[File, File], info: Map[File, F])(implicit infoFormat: Format[F]): Unit =
		IO.gzipFileOut(file) { out =>
			write(out, (relation, info) )
		}

	type RelationInfo[F] = (Relation[File,File], Map[File, F])

	def readInfo[F <: FileInfo](file: File)(implicit infoFormat: Format[F]): RelationInfo[F] =
		try { readUncaught(file)(infoFormat) }
		catch { case e: IOException => (Relation.empty, Map.empty) }

	def readUncaught[F <: FileInfo](file: File)(implicit infoFormat: Format[F]): RelationInfo[F] =
		IO.gzipFileIn(file) { in =>
			read[RelationInfo[F]](in)
		}
}