/* NSC -- new Scala compiler
 * Copyright 2005-2009 LAMP/EPFL
 * @author  Martin Odersky
 */
// $Id: ScriptRunner.scala 18610 2009-08-30 20:42:17Z extempore $

package scala.tools.nsc

import java.io.{
  InputStream, OutputStream,
  BufferedReader, FileInputStream, FileOutputStream,
  FileReader, InputStreamReader, PrintWriter, FileWriter,
  IOException
}
import scala.io.{ Directory, File, Path }
// import scala.io.arm.ManagedResource
import java.io.{ File => JFile }
import java.lang.reflect.InvocationTargetException
import java.net.URL
import java.util.jar.{ JarEntry, JarOutputStream }
import java.util.regex.Pattern

import scala.tools.nsc.io.PlainFile
import scala.tools.nsc.reporters.{Reporter,ConsoleReporter}
import scala.tools.nsc.util.{ClassPath, CompoundSourceFile, BatchSourceFile, SourceFile, SourceFileFragment}

/** An object that runs Scala code in script files.
 *
 *  <p>For example, here is a complete Scala script on Unix:</pre>
 *  <pre>
 *    #!/bin/sh
 *    exec scala "$0" "$@"
 *    !#
 *    Console.println("Hello, world!")
 *    argv.toList foreach Console.println
 *  </pre>
 *  <p>And here is a batch file example on Windows XP:</p>
 *  <pre>
 *    ::#!
 *    @echo off
 *    call scala %0 %*
 *    goto :eof
 *    ::!#
 *    Console.println("Hello, world!")
 *    argv.toList foreach Console.println
 *  </pre>
 *
 *  @author  Lex Spoon
 *  @version 1.0, 15/05/2006
 *  @todo    It would be better if error output went to stderr instead
 *           of stdout...
 */
object ScriptRunner
{
  /* While I'm chasing down the fsc and script bugs. */
  def DBG(msg: Any) {
    System.err.println(msg.toString)
    System.err.flush()
  }
  
  /** Default name to use for the wrapped script */
  val defaultScriptMain = "Main"
  
  private def addShutdownHook(body: => Unit) =
    Runtime.getRuntime addShutdownHook new Thread { override def run { body } }

  /** Pick a main object name from the specified settings */
  def scriptMain(settings: Settings) = settings.script.value match {
    case "" => defaultScriptMain
    case x  => x
  }

  /** Choose a jar filename to hold the compiled version of a script. */
  private def jarFileFor(scriptFile: String): File = {
    val name =
      if (scriptFile endsWith ".jar") scriptFile
      else scriptFile + ".jar"
    
    File(name)
  }
  
  def copyStreams(in: InputStream, out: OutputStream) = {
    val buf = new Array[Byte](10240)
    
    def loop: Unit = in.read(buf, 0, buf.length) match {
      case -1 => in.close()
      case n  => out.write(buf, 0, n) ; loop
    }
    
    loop
  }

  /** Try to create a jar file out of all the contents
   *  of the directory <code>sourcePath</code>.
   */
  private def tryMakeJar(jarFile: File, sourcePath: Directory) = {
    def addFromDir(jar: JarOutputStream, dir: Directory, prefix: String) {
      def addFileToJar(entry: File) = {
        jar putNextEntry new JarEntry(prefix + entry.name)
        copyStreams(entry.inputStream, jar)
        jar.closeEntry
      }

      dir.list foreach { entry =>
        if (entry.isFile) addFileToJar(entry.toFile)
        else addFromDir(jar, entry.toDirectory, prefix + entry.name + "/")
      }
    }

    try {
      val jar = new JarOutputStream(jarFile.outputStream())
      addFromDir(jar, sourcePath, "")
      jar.close
    } 
    catch {
      case _: Error => jarFile.delete() // XXX what errors to catch?
    }
  }

  /** Read the entire contents of a file as a String. */
  private def contentsOfFile(filename: String) = File(filename).slurp()

  /** Find the length of the header in the specified file, if
    * there is one.  The header part starts with "#!" or "::#!"
    * and ends with a line that begins with "!#" or "::!#".
    */
  private def headerLength(filename: String): Int = {
    val headerPattern = Pattern.compile("""^(::)?!#.*(\r|\n|\r\n)""", Pattern.MULTILINE)
    val fileContents = contentsOfFile(filename)
    def isValid = List("#!", "::#!") exists (fileContents startsWith _)
    
    if (!isValid) 0 else {
      val matcher = headerPattern matcher fileContents
      if (matcher.find) matcher.end
      else throw new IOException("script file does not close its header with !# or ::!#")
    }
  }

  /** Split a fully qualified object name into a
   *  package and an unqualified object name */
  private def splitObjectName(fullname: String): (Option[String], String) =
    (fullname lastIndexOf '.') match {
      case -1   => (None, fullname)
      case idx  => (Some(fullname take idx), fullname drop (idx + 1))
    }

  /** Code that is added to the beginning of a script file to make
   *  it a complete Scala compilation unit.
   */
  protected def preambleCode(objectName: String): String = {
    val (maybePack, objName)  = splitObjectName(objectName)
    val packageDecl           = maybePack map ("package %s\n" format _) getOrElse ("")

    return """|
    |  object %s {
    |    def main(argv: Array[String]): Unit = {
    |      val args = argv
    |      new AnyRef {
    |""".stripMargin.format(objName)
  }

  /** Code that is added to the end of a script file to make
   *  it a complete Scala compilation unit.
   */
  val endCode = """
    |      }
    |    }
    |  }
    |""".stripMargin

  /** Wrap a script file into a runnable object named
   *  <code>scala.scripting.Main</code>.
   */
  def wrappedScript(
    objectName: String, 
    filename: String, 
    getSourceFile: PlainFile => BatchSourceFile): SourceFile = 
  {
    val preamble = new BatchSourceFile("<script preamble>", preambleCode(objectName).toCharArray)
    val middle = {
      val bsf = getSourceFile(PlainFile fromPath filename)
      new SourceFileFragment(bsf, headerLength(filename), bsf.length)
    }
    val end = new BatchSourceFile("<script trailer>", endCode.toCharArray)

    new CompoundSourceFile(preamble, middle, end)
  }

  /** Compile a script using the fsc compilation deamon.
   *
   *  @param settings     ...
   *  @param scriptFileIn ...
   *  @return             ...
   */
  private def compileWithDaemon(
      settings: GenericRunnerSettings,
      scriptFileIn: String): Boolean =
  {
    val scriptFile = CompileClient absFileName scriptFileIn
    
    {
      import settings._
      for (setting <- List(classpath, sourcepath, bootclasspath, extdirs, outdir)) {
        // DBG("%s = %s".format(setting.name, setting.value))
        setting.value = CompileClient absFileName setting.value
      }
    }
      
    val compSettingNames  = new Settings(error).allSettings map (_.name)
    val compSettings      = settings.allSettings filter (compSettingNames contains _.name)
    val coreCompArgs      = compSettings flatMap (_.unparse)
    val compArgs          = coreCompArgs ::: List("-Xscript", scriptMain(settings), scriptFile)
    var compok            = true
        
    // XXX temporary as I started using ManagedResource not remembering it wasn't checked in.
    def ManagedResource[T](x: => T) = Some(x)
    
    for {
      socket <- ManagedResource(CompileSocket getOrCreateSocket "")
      val _ = if (socket == null) return false
      out <- ManagedResource(new PrintWriter(socket.getOutputStream(), true))
      in <- ManagedResource(new BufferedReader(new InputStreamReader(socket.getInputStream())))
    } {
      out println (CompileSocket getPassword socket.getPort)
      out println (compArgs mkString "\0")
      
      for (fromServer <- (Iterator continually in.readLine()) takeWhile (_ != null)) {
        Console.err println fromServer
        if (CompileSocket.errorPattern matcher fromServer matches)
          compok = false
      }
      // XXX temp until managed resource is available
      in.close() ; out.close() ; socket.close()
    }
    
    compok
  }

  protected def newGlobal(settings: Settings, reporter: Reporter) =
    new Global(settings, reporter)

  /** Compile a script and then run the specified closure with
    * a classpath for the compiled script.
    *
    * @returns true if compilation and the handler succeeds, false otherwise.
    */
  private def withCompiledScript(
    settings: GenericRunnerSettings,
    scriptFile: String)
    (handler: String => Boolean): Boolean =
  {
    /** Compiles the script file, and returns the directory with the compiled
     *  class files, if the compilation succeeded.
     */
    def compile: Option[Directory] = {
      val compiledPath = Directory makeTemp "scalascript"

      // delete the directory after the user code has finished
      addShutdownHook(compiledPath.deleteRecursively())

      settings.outdir.value = compiledPath.path

      if (settings.nocompdaemon.value) {
        val reporter = new ConsoleReporter(settings)
        val compiler = newGlobal(settings, reporter)
        val cr = new compiler.Run
        val wrapped = wrappedScript(scriptMain(settings), scriptFile, compiler getSourceFile _)
        
        cr compileSources List(wrapped)
        if (reporter.hasErrors) None else Some(compiledPath)
      }
      else if (compileWithDaemon(settings, scriptFile)) Some(compiledPath)
      else None  	      
    }

    if (settings.savecompiled.value) {
      val jarFile = jarFileFor(scriptFile)
      def jarOK   = jarFile.canRead && (jarFile isFresher File(scriptFile))
      
      def recompile() = {
        jarFile.delete()
        
        compile match {
          case Some(compiledPath) =>
            tryMakeJar(jarFile, compiledPath)
            if (jarOK) {
              compiledPath.deleteRecursively()
              handler(jarFile.toAbsolute.path)
            }            
            // jar failed; run directly from the class files
            else handler(compiledPath.path)
          case _  => false
        }
      }

      if (jarOK) handler(jarFile.toAbsolute.path) // pre-compiled jar is current
      else recompile()                            // jar old - recompile the script.
    }
    // don't use a cache jar at all--just use the class files
    else compile map (cp => handler(cp.path)) getOrElse false
  }

  /** Run a script after it has been compiled 
   *
   * @returns true if execution succeeded, false otherwise
   */
  private def runCompiled(
    settings: GenericRunnerSettings,
		compiledLocation: String,
		scriptArgs: List[String]): Boolean =
	{
    def fileToURL(f: JFile): Option[URL] =
      try Some(f.toURL) catch { case _: Exception => None }

    def paths(str: String, expandStar: Boolean): List[URL] =
      for {
        file <- ClassPath.expandPath(str, expandStar) map (new JFile(_))
        if file.exists 
        url <- fileToURL(file)
      } yield url

    val classpath =
      (paths(settings.bootclasspath.value, true) :::
       paths(compiledLocation, false) :::
       paths(settings.classpath.value, true))

    try {
      ObjectRunner.run(
        classpath,
        scriptMain(settings),
        scriptArgs.toArray)
      true
    } 
    catch {
      case e @ (_: ClassNotFoundException | _: NoSuchMethodException) =>
        Console println e
        false
      case e: InvocationTargetException =>
        e.getCause.printStackTrace
        false
    }
  }

  /** Run a script file with the specified arguments and compilation
   *  settings.
   *
   * @returns true if compilation and execution succeeded, false otherwise.
   */
  def runScript(
    settings: GenericRunnerSettings,
		scriptFile: String,
		scriptArgs: List[String]): Boolean =
	{
	  if (File(scriptFile).isFile)
	    withCompiledScript(settings, scriptFile) { runCompiled(settings, _, scriptArgs) }
	  else
	    throw new IOException("no such file: " + scriptFile)
  }

  /** Run a command 
   *
   * @returns true if compilation and execution succeeded, false otherwise.
   */
  def runCommand(
    settings: GenericRunnerSettings,
    command: String,
		scriptArgs: List[String]) : Boolean =
	{
    val scriptFile = File.makeTemp("scalacmd", ".scala")
    // save the command to the file
    scriptFile writeAll List(command)
    
    try withCompiledScript(settings, scriptFile.path) { runCompiled(settings, _, scriptArgs) }
    finally scriptFile.delete()  // in case there was a compilation error
  }
}