//
// Copyright (c) 2019, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//    08 May 19  Matthew Giannini Creation
//

using [java] java.net::URI
using [java] java.nio.file
using [java] fanx.interop::Interop

**************************************************************************
** ClassLib
**************************************************************************

**
** ClassLib models a file that contains java packages and class files.
**
abstract class ClassLib
{
  new make(File file) { this.file = file }

  ** The file to load package from.
  const File file

  ** Load packages keyed by package name in "." format.
  abstract Str:ClassPathPackage loadPackages()

  ** Release any resources that the library may have opened when loading
  ** the packages. Return this.
  virtual This close() { return this }

  override Str toStr() { file.toStr }

//////////////////////////////////////////////////////////////////////////
// Util
//////////////////////////////////////////////////////////////////////////

  ** Common utility to determine if a given '.class' file should be accepted or not.
  ** If it is accepted it will be added to the given accumulator.
  protected Void accept(Str:ClassPathPackage acc, Uri uri, File f, Bool isBoot := false)
  {
    // don't care about anything but .class files
    if (uri.ext != "class") return

    // convert URI to package name, skip non-public 'com.sun' if rt.jar
    packageName := uri.path[0..-2].join(".")
    if (isBoot)
    {
      if (packageName.startsWith("com.sun") || packageName.startsWith("sun"))
        return
    }

    // get simple name of class
    name := uri.basename
    if (name == "Void") return

    // get or add package
    package := acc[packageName]
    if (package == null) acc[packageName] = package = ClassPathPackage(packageName)

    // add class to package if not already defined
    if (package.classes[name] == null) package.classes[name] = f
  }
}

**************************************************************************
** JarClassLib
**************************************************************************

**
** JarClassLib can load packages from JAR files, or from directories
** on the file system that represent an "exploded" JAR file.
**
final class JarClassLib : ClassLib
{
  new make(File file) : super(file)
  {
  }

  private Zip? zip := null

  once override Str:ClassPathPackage loadPackages()
  {
    acc := Str:ClassPathPackage[:]
    if (this.file.isDir)
    {
      file.walk |File x| { accept(acc, x.uri.relTo(file.uri), x) }
    }
    else
    {
      try
      {
        this.zip = Zip.open(this.file)
        isBoot := file.name == "rt.jar"
        zip.contents.each |File x, Uri uri| { accept(acc, uri, x, isBoot) }
      }
      catch (Err e)
      {
        echo("ERROR: $typeof: $file")
        e.trace
      }
    }
    return acc
  }

  override This close()
  {
    zip?.close
    return super.close
  }

//////////////////////////////////////////////////////////////////////////
// System Libs
//////////////////////////////////////////////////////////////////////////

  ** Find all jars in system classpath (Java 1.8 and earlier)
  static JarClassLib[] findClassicLibs()
  {
    libs := JarClassLib[,]

    // System.property "sun.boot.class.path"; this is preferable
    // to trying to figure out rt.jar - on platforms like Mac OS X
    // the classes are in very non-standard locations
    Env.cur.vars.get("sun.boot.class.path", "").split(File.pathSep[0]).each |Str path|
    {
      f := File.os(path)
      if (!f.exists) return
      if (!f.isDir && f.ext != "jar") return
      if (javaIgnore[f.name] != null) return
      libs.add(JarClassLib(f))
    }

    // {java}lib/rt.jar (only if sun.boot.class.path failed)
    lib := File.os(Env.cur.vars.get("java.home", "") + File.sep + "lib")
    if (libs.isEmpty)
    {
      rt := lib + `rt.jar`
      if (rt.exists) libs.add(JarClassLib(rt))
    }

    // {java}lib/ext
    lib.plus(`ext/`).list.each |f|
    {
      if (f.ext != "jar") return
      if (javaIgnore[f.name] != null) return
      libs.add(JarClassLib(f))
    }

    return libs
  }

  // ignore the common big jars that ship with
  // HotSpot which don't contain public java packages
  private static const Str:Str javaIgnore := [:].addList(
  [
    "deploy.jar",
    "charsets.jar",
    "javaws.jar",
    "jsse.jar",
    "resources.jar",
    "dnsns.jar",
    "localedata.jar",
    "sunec.jar",
    "sunec_provider.jar",
    "sunjce_provider.jar",
    "sunmscapi.jar",
    "sunpkcs11.jar",
    "zipfs.jar",
  ])
}

**************************************************************************
** ModuleClassLib
**************************************************************************

**
** ModuleClassLib can load packages and class files from java modules.
**
final class ModuleClassLib : ClassLib
{
  new make(File file) : super(file)
  {

  }

  once override Str:ClassPathPackage loadPackages()
  {
    acc := Str:ClassPathPackage[:]
    file.walk |File x|
    {
      // get an absolute Uri for the just package portion of the module path
      //   Example: jrt:/module/java.base/java/lang/ => /java/lang/
      uri := `/`.plus(x.uri.relTo(file.uri))
      accept(acc, uri, x, true)
    }
    return acc
  }

//////////////////////////////////////////////////////////////////////////
// System Libs
//////////////////////////////////////////////////////////////////////////

  static ModuleClassLib[] findModuleLibs()
  {
    fs := FileSystems.getFileSystem(URI.create("jrt:/"))
    modules := Interop.toFan(Files.list(fs.getPath("/modules", Str[,])).iterator)

    return modules.findAll |Path path->Bool| {
      f := Interop.toFan(path)
      if (!f.isDir) return false

      moduleName := f.name

      // JDK modules
      if (moduleName.startsWith("jdk."))
      {
        // ignore all JDK modules except jdk.management
        if (!moduleName.startsWith("jdk.management")) return false
      }

      return true
    }
    .map |Path path->ModuleClassLib| { ModuleClassLib(Interop.toFan(path)) }
  }
}

**************************************************************************
** ClassPathPackage
**************************************************************************

**
** ClassPathPackage models a single package found in the class
** path with a map of classnames to ClassFiles.
**
class ClassPathPackage
{
  new make(Str name) { this.name = name }

  ** Package name in "." format
  const Str name

  ** Classfiles keyed by simple name (not qualified name)
  Str:File classes := [:] { private set }

  ** Return name
  override Str toStr() { name }
}