/* NSC -- new Scala compiler
 * Copyright 2005-2009 LAMP/EPFL
 * @author Paul Phillips
 */
// $Id$

//
// TODO, if practical:
//
// 1) Types: val s: String = x.<tab> should only show members which result in a String.
//      Possible approach: evaluate buffer as if current identifier is 
// 2) Implicits: x.<tab> should show not only x's members but those of anything for which
//      there is an implicit conversion from x.
// 3) Chaining: x.foo(bar).<tab> should complete on foo's result type.
// 4) Imports: after import scala.collection.mutable._, HashMap should be among
//      my top level identifiers.
// 5) Caching: it's silly to parse all the jars on every startup, we should have
//      a peristent store somewhere we can write and only check last-mod dates.
// 6) Security: Are we using the filesystem unnecessarily?
//

package scala.tools.nsc
package interpreter

import jline._
import java.net.URL
import java.util.concurrent.ConcurrentHashMap
import scala.concurrent.DelayedLazyVal

// REPL completor - queries supplied interpreter for valid completions
// based on current contents of buffer.
class Completion(val interpreter: Interpreter) extends Completor {
  import Completion._
  import java.util.{ List => JList }
  import interpreter.compilerClasspath
  
  // it takes a little while to look through the jars so we use a future and a concurrent map
  class CompletionAgent {
    val dottedPaths = new ConcurrentHashMap[String, List[String]]
    val topLevelPackages = new DelayedLazyVal(
      () => enumToList(dottedPaths.keys) filterNot (_ contains '.'),
      getDottedPaths(dottedPaths, interpreter)
    )
  }
  val agent = new CompletionAgent
  import agent._  

  // One instance of a command line
  class Buffer(s: String) {
    val buffer = if (s == null) "" else s
    val segments = buffer.split("\\.", -1).toList
    val lastDot = buffer.lastIndexOf('.')
    val hasDot = segments.size > 0 && segments.last == ""
    
    // given foo.bar.baz, path = foo.bar and stub = baz
    val (path, stub) = segments.size match {
      case 0    => ("", "")
      case 1    => (segments.head, "")
      case _    => (segments.init.mkString("."), segments.last)
    }

    def filt(xs: List[String]) = xs filter (_ startsWith stub)

    case class Result(candidates: List[String], position: Int) {
      def getCandidates() = (candidates map (_.trim) removeDuplicates) sort (_ < _)
    }
    
    // work out completion candidates and position
    def analyzeBuffer(clist: JList[String]): Result = {
      lazy val ids = idsStartingWith(path)
      lazy val pkgs = pkgsStartingWith(path)
      lazy val count = (ids ::: pkgs).size        
      
      def doSimple(): Result = count match {
        case 0                                    => Result(Nil, 0)
        case 1 if pkgs.size > 0                   => Result(pkgs, 0)
        case 1 if buffer.length < ids.head.length => Result(ids, 0)
        case 1                                    => Result(ids, 0)
          // XXX for now commented out "dot inference" because it's overcomplicated
          // val members = membersOfId(ids.head) filter (_ startsWith stub)
          // if (members.isEmpty) Result(Nil, 0)
          // else Result(members, path.length + 1)
        case _                                    => Result(ids ::: pkgs, 0)
      }
      
      // a few keywords which don't appear as methods via reflection
      val memberKeywords = List("isInstanceOf", "asInstanceOf")      
      def doDotted(): Result = {
        lazy val pkgs = filt(membersOfPath(path))
        lazy val ids = filt(membersOfId(path))
        lazy val idExtras = filt(memberKeywords)  // isInstanceOf and asInstanceOf
        lazy val statics = filt(completeStaticMembers(path))
        
        if (!pkgs.isEmpty) Result(pkgs, path.length + 1)
        else if (!ids.isEmpty) Result(ids ::: idExtras, path.length + 1)
        else Result(statics ::: idExtras, path.length + 1)
      }
      
      segments.size match {
        case 0            => Result(Nil, 0)
        case 1            => doSimple()
        case _            => doDotted()
      }
    }
    
    def isValidId(s: String) = interpreter.unqualifiedIds contains s
    def membersOfId(s: String) = interpreter membersOfIdentifier s
    
    def isValidPath(s: String) = dottedPaths containsKey s
    def membersOfPath(s: String) = if (isValidPath(s)) dottedPaths get s else Nil
    
    // XXX generalize this to look through imports
    def membersOfScala() = membersOfPath("scala")
    def membersOfJavaLang() = membersOfPath("java.lang")
    def membersOfPredef() = membersOfId("scala.Predef")
    def defaultMembers = {
      val xs = membersOfScala ::: membersOfJavaLang ::: membersOfPredef
      val excludes = List("""Tuple\d+""".r, """Product\d+""".r, """Function\d+""".r,
        """.*Exception$""".r, """.*Error$""".r)
      xs filter (x => excludes forall (r => r.findFirstMatchIn(x).isEmpty))
    }
    
    def pkgsStartingWith(s: String) = topLevelPackages() filter (_ startsWith s)
    def idsStartingWith(s: String) = (interpreter.unqualifiedIds ::: defaultMembers) filter (_ startsWith s)

    def complete(clist: JList[String]): Int = {
      val res = analyzeBuffer(clist)
      res.getCandidates foreach (clist add _)
      res.position
    }
  }
  
  // jline's completion comes through here - we ask a Buffer for the candidates.
  override def complete(_buffer: String, cursor: Int, candidates: JList[String]): Int =
    new Buffer(_buffer).complete(candidates)
  
  def completeStaticMembers(path: String): List[String] = {
    import java.lang.reflect.Modifier.{ isPrivate, isProtected, isStatic }
    def isVisible(x: Int) = !isPrivate(x) && !isProtected(x)
    def isSingleton(x: Int, isJava: Boolean) = !isJava || isStatic(x)
    
    def getMembers(c: Class[_], isJava: Boolean): List[String] =
      c.getMethods.toList .
        filter (x => isVisible(x.getModifiers)) .
        filter (x => isSingleton(x.getModifiers, isJava)) .
        map (_.getName) .
        filter (isValidCompletion)
    
    def getClassObject(path: String): Option[Class[_]] =
      (interpreter getClassObject path) orElse
      (interpreter getClassObject ("scala." + path)) orElse
      (interpreter getClassObject ("java.lang." + path))
        
    // java style, static methods    
    val js = getClassObject(path) map (getMembers(_, true)) getOrElse Nil
    // scala style, methods on companion object
    val ss = getClassObject(path + "$") map (getMembers(_, false)) getOrElse Nil
    
    js ::: ss
  }
}

object Completion
{
  import java.io.File
  import java.util.jar.{ JarEntry, JarFile }
  
  def enumToList[T](e: java.util.Enumeration[T]): List[T] = enumToList(e, Nil)
  def enumToList[T](e: java.util.Enumeration[T], xs: List[T]): List[T] =
    if (e == null || !e.hasMoreElements) xs else enumToList(e, e.nextElement :: xs)
  
  // methods to leave out of completion
  val excludeMethods = List("", "hashCode", "equals", "wait", "notify", "notifyAll")  

  private def exists(path: String) = new File(path) exists
  
  def isValidCompletion(x: String) = !(x contains "$$") && !(excludeMethods contains x)
  def isClass(x: String)    = x endsWith ".class"
  def dropClass(x: String)  = x.substring(0, x.length - 6)  // drop .class
  
  def getClassFiles(path: String): List[String] = {
    if (!exists(path)) return Nil

    enumToList(new JarFile(path).entries) .
      map (_.getName) .
      filter (isClass) .
      map (dropClass) .
      filter (isValidCompletion)
  }
  
  // all the dotted path to classfiles we can find by poking through the jars
  def getDottedPaths(
    map: ConcurrentHashMap[String, List[String]],
    interpreter: Interpreter): Unit =
  {
    val cp =
      interpreter.compilerClasspath.map(_.getPath) :::            // compiler jars, scala-library.jar etc.
      interpreter.settings.bootclasspath.value.split(':').toList  // boot classpath, java.lang.* etc.
    
    val jars = cp.removeDuplicates filter (_ endsWith ".jar")
    
    // for e.g. foo.bar.baz.C, returns (foo -> bar), (foo.bar -> baz), (foo.bar.baz -> C)
    // and scala.Range$BigInt needs to go scala -> Range -> BigInt
    def subpaths(s: String): List[(String, String)] = {
      val segs = s.split('.')
      for (i <- List.range(0, segs.length - 1)) yield {
        val k = segs.take(i+1).mkString(".")
        val v = segs(i+1)
        (k -> v)
      }
    }
    
    def oneJar(jar: String): Unit = {
      def cleanup(s: String): String = s map { c => if (c == '/' || c == '$') '.' else c } toString
      val classfiles = Completion getClassFiles jar map cleanup
        
      for (cl <- classfiles; (k, v) <- subpaths(cl)) {
        if (map containsKey k) map.put(k, v :: map.get(k))
        else map.put(k, List(v))
      }
    }
    
    jars foreach oneJar
  }
}