/* 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)
    }
}