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

import java.io.File
import java.util.zip.{ZipException, ZipFile}
import Function.const

object Locate
{
	type DefinesClass = File => String => Boolean

	/** Right(src) provides the value for the found class
	* Left(true) means that the class was found, but it had no associated value
	* Left(false) means that the class was not found */
	def value[S](classpath: Seq[File], get: File => String => Option[S]): String => Either[Boolean, S] =
	{
		val gets = classpath.toStream.map(getValue(get))
		className => find(className, gets)
	}
	
	def find[S](name: String, gets: Stream[String => Either[Boolean, S]]): Either[Boolean, S] =
		if(gets.isEmpty)
			Left(false)
		else
			gets.head(name) match
			{
				case Left(false) => find(name, gets.tail)
				case x => x
			}
	
	/** Returns a function that searches the provided class path for
	* a class name and returns the entry that defines that class.*/
	def entry(classpath: Seq[File], f: DefinesClass): String => Option[File] =
	{
		val entries = classpath.toStream.map { entry => (entry, f(entry)) }
		className => entries collect { case (entry, defines) if defines(className) => entry } headOption;
	}
	def resolve(f: File, className: String): File = if(f.isDirectory) classFile(f, className) else f
	
	def getValue[S](get: File => String => Option[S])(entry: File): String => Either[Boolean, S] = 
	{
		val defClass = definesClass(entry)
		val getF = get(entry)
		className => if(defClass(className)) getF(className).toRight(true) else Left(false)
	}
		
	def definesClass(entry: File): String => Boolean =
		if(entry.isDirectory)
			directoryDefinesClass(entry)
		else if(entry.exists && classpath.ClasspathUtilities.isArchive(entry, contentFallback=true))
			jarDefinesClass(entry)
		else
			const(false)
	
	def jarDefinesClass(entry: File): String => Boolean =
	{
		import collection.JavaConversions._
		val jar = try { new ZipFile(entry, ZipFile.OPEN_READ) } catch {
			// ZipException doesn't include the file name :(
			case e: ZipException => throw new RuntimeException("Error opening zip file: " + entry.getName, e)
		}
		val entries = try { jar.entries.map(e => toClassName(e.getName)).toSet } finally { jar.close() }
		entries.contains _
	}
	
	def toClassName(entry: String): String =
		entry.stripSuffix(ClassExt).replace('/', '.')
	
	val ClassExt = ".class"
		
	def directoryDefinesClass(entry: File): String => Boolean =
		className => classFile(entry, className).isFile
		
	def classFile(baseDir: File, className: String): File = 
	{
		val (pkg, name) = components(className)
		val dir = subDirectory(baseDir, pkg)
		new File(dir, name + ClassExt)
	}
		
	def subDirectory(base: File, parts: Seq[String]): File =
		(base /: parts) ( (b, p) => new File(b,p) )
		
	def components(className: String): (Seq[String], String) =
	{
		assume(!className.isEmpty)
		val parts = className.split("\\.")
		if(parts.length == 1) (Nil, parts(0)) else (parts.init, parts.last)
	}
}