/*
   Copyright 2010 Aaron J. Radke

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
// vim: set ts=3 sw=3 et:

package cc.drx

object Classpath{
  def apply(paths:Iterable[File]):Classpath = new Classpath(paths.toList.distinct) //distict prevents double inclusion
  def apply():Classpath = new Classpath(Nil)
  //[How to print out the current project classpath](https://www.mkyong.com/java/how-to-print-out-the-current-project-classpath/)
  def apply(cl:java.lang.ClassLoader):Classpath = {
    //Note: there are good reasons why this is not an existing functionality so consider it a hack
    //java 9 and above made it clear the type of class loader needs to be evaluated...
    //java 9 and up the classlaoader is not by default an instance of the URLClassLoader
    //see https://stackoverflow.com/a/46709196/622016
    cl match {
      //-- java8 style
      case x:java.net.URLClassLoader => apply(x.getURLs)
      case _ =>
        val loaderName = cl.getClass.getName

        //-- java9 style hack to get cp inspired from https://stackoverflow.com/q/49557431/622016
        //Note: requires java opts --add-opens java.base/jdk.internal.loader=ALL-UNNAMED
        if(loaderName endsWith "jdk.internal.loader.ClassLoaders$AppClassLoader"){ //java10 name...
          Try{
            //--unsafe reflection trickery to get internal java9/10 apis's for the urls
            val ucp:Any = JailBreak(cl)("ucp")
            JailBreak(ucp,"jdk.internal.loader.URLClassPath").invoke[Array[java.net.URL]]("getURLs")
          } match {
            case Success(urls) =>
              apply(urls)
            case Failure(e) =>
              Console.err.println(s"Error: $e")
              Console.err.println("Note using this hack for java9 or above requires java options flags: `--add-opens java.base/jdk.internal.loader=ALL-UNNAMED`")
              defaultClasspathWithWarning
          }
        }
        //--try falling back to java properties
        else{
          Console.err.println(s"Warning: $loaderName is not a URLClassLoader to obtain obtain a classpath from URLs")
          defaultClasspathWithWarning //return the empty classpath... for now instead of an exception
        }
    }
  }

  private def defaultClasspathWithWarning:Classpath = {
    Console.err.println(s"Warning: could not get a URL classloader to collect a classPath")
    Console.err.println(s"Note: falling back to the Java property java.class.path")
    fromJavaProperties
  }

  private def apply(urls:Array[java.net.URL]):Classpath = apply(urls.map{url => File(url.getFile)})

  //-- system options
  def system:Classpath = apply(java.lang.ClassLoader.getSystemClassLoader())
  def local:Classpath = apply(getClass.getClassLoader)
  def fromJavaProperties:Classpath = {
    Option(System.getProperty("java.class.path")) match {
      case Some(cpProperty) =>
        val paths = cpProperty split System.getProperty("path.separator") map File.apply
        apply(paths)
      case None => //null property case
        Console.err.println(s"Warning: No java.class.path property has been set.")
        Classpath(Nil) //fall back to an empty classpath
    }
  }
}

class Classpath(val files:List[File]){
  // 2017-07-30("use files instead", "dk")
  // def cp = files
  def +(f:File) = Classpath(files :+ f)
  def ++(that:Classpath):Classpath = Classpath(files ++ that.files)
  def ++(that:Iterable[File]) = Classpath(files ++ that)
  private val sep = if(OS.kind == OS.Windows) ";" else ":"
  override def toString = files.map{_.abs.path} mkString sep //TODO handle spaces???

  def nice:String = {
    if(files.isEmpty) ""
    else {
      val maxWidth = files.map{_.name.size}.max
      val lines = for(f <- files; dir <- f.parent) yield {
        def bad(c:Char) = c == '_' || c == '.' || c.isDigit
        val (pre,post) = f.name span {c => !bad(c)}
        val name = Color.Green.ansi(pre)+post
        name.fit(maxWidth+2) + dir.path + (if(dir.isFile || dir.isDir) "" else Red ansi " missing")
      }
      lines mkString "\n"
    }
  }

  //TODO make a walk
  // def walk:Iterable[File] = ???
}