/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
package org.apache.metron.stellar.common.utils;

import java.io.File;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Optional;
import org.apache.accumulo.start.classloader.vfs.UniqueFileReplicator;
import org.apache.commons.vfs2.CacheStrategy;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.FileType;
import org.apache.commons.vfs2.cache.SoftRefFilesCache;
import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
import org.apache.commons.vfs2.impl.FileContentInfoFilenameFactory;
import org.apache.commons.vfs2.impl.VFSClassLoader;
import org.apache.commons.vfs2.provider.hdfs.HdfsFileProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VFSClassloaderUtil {
  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  /**
   * Create a FileSystem manager suitable for our purposes.
   * This manager supports files of the following types:
   * * res - resource files
   * * jar
   * * tar
   * * bz2
   * * tgz
   * * zip
   * * HDFS
   * * FTP
   * * HTTP/S
   * * file
   * @return vfs
   * @throws FileSystemException
   */
  public static FileSystemManager generateVfs() throws FileSystemException {
    DefaultFileSystemManager vfs = new DefaultFileSystemManager();
    vfs.addProvider("res", new org.apache.commons.vfs2.provider.res.ResourceFileProvider());
    vfs.addProvider("zip", new org.apache.commons.vfs2.provider.zip.ZipFileProvider());
    vfs.addProvider("gz", new org.apache.commons.vfs2.provider.gzip.GzipFileProvider());
    vfs.addProvider("ram", new org.apache.commons.vfs2.provider.ram.RamFileProvider());
    vfs.addProvider("file", new org.apache.commons.vfs2.provider.local.DefaultLocalFileProvider());
    vfs.addProvider("jar", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
    vfs.addProvider("http", new org.apache.commons.vfs2.provider.http.HttpFileProvider());
    vfs.addProvider("https", new org.apache.commons.vfs2.provider.https.HttpsFileProvider());
    vfs.addProvider("ftp", new org.apache.commons.vfs2.provider.ftp.FtpFileProvider());
    vfs.addProvider("ftps", new org.apache.commons.vfs2.provider.ftps.FtpsFileProvider());
    vfs.addProvider("war", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
    vfs.addProvider("par", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
    vfs.addProvider("ear", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
    vfs.addProvider("sar", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
    vfs.addProvider("ejb3", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
    vfs.addProvider("tmp", new org.apache.commons.vfs2.provider.temp.TemporaryFileProvider());
    vfs.addProvider("tar", new org.apache.commons.vfs2.provider.tar.TarFileProvider());
    vfs.addProvider("tbz2", new org.apache.commons.vfs2.provider.tar.TarFileProvider());
    vfs.addProvider("tgz", new org.apache.commons.vfs2.provider.tar.TarFileProvider());
    vfs.addProvider("bz2", new org.apache.commons.vfs2.provider.bzip2.Bzip2FileProvider());
    vfs.addProvider("hdfs", new HdfsFileProvider());
    vfs.addExtensionMap("jar", "jar");
    vfs.addExtensionMap("zip", "zip");
    vfs.addExtensionMap("gz", "gz");
    vfs.addExtensionMap("tar", "tar");
    vfs.addExtensionMap("tbz2", "tar");
    vfs.addExtensionMap("tgz", "tar");
    vfs.addExtensionMap("bz2", "bz2");
    vfs.addMimeTypeMap("application/x-tar", "tar");
    vfs.addMimeTypeMap("application/x-gzip", "gz");
    vfs.addMimeTypeMap("application/zip", "zip");
    vfs.setFileContentInfoFactory(new FileContentInfoFilenameFactory());
    vfs.setFilesCache(new SoftRefFilesCache());
    vfs.setReplicator(new UniqueFileReplicator(new File(System.getProperty("java.io.tmpdir"))));
    vfs.setCacheStrategy(CacheStrategy.ON_RESOLVE);
    vfs.init();
    return vfs;
  }

  /**
   * Create a classloader backed by a virtual filesystem which can handle the following URI types:
   * * res - resource files
   * * jar
   * * tar
   * * bz2
   * * tgz
   * * zip
   * * HDFS
   * * FTP
   * * HTTP/S
   * * file
   * @param paths A set of comma separated paths.  The paths are URIs or URIs with a regex pattern at the end.
   * @return A classloader object if it can create it
   * @throws FileSystemException
   */
  public static Optional<ClassLoader> configureClassloader(String paths) throws FileSystemException {
    LOG.debug("Configuring class loader with paths = {}", paths);
    if(paths.trim().isEmpty()) {
      LOG.debug("No paths provided. Not returning a ClassLoader.");
      return Optional.empty();
    }
    FileSystemManager vfs = generateVfs();
    FileObject[] objects = resolve(vfs, paths);
    if(objects == null || objects.length == 0) {
      LOG.debug("No Classloader able to be resolved from provided paths. Not returning a ClassLoader.");
      return Optional.empty();
    }
    LOG.debug("vfs = {}, objects = {}", vfs, objects);
    return Optional.of(new VFSClassLoader(objects, vfs, vfs.getClass().getClassLoader()));
  }

  /**
   * Resolve a set of URIs into FileObject objects.
   * This is not recursive. The URIs can refer directly to a file or directory or an optional regex at the end.
   * (NOTE: This is NOT a glob).
   * @param vfs The file system manager to use to resolve URIs
   * @param uris comma separated URIs and URI + globs
   * @return
   * @throws FileSystemException
   */
  static FileObject[] resolve(FileSystemManager vfs, String uris) throws FileSystemException {
    if (uris == null) {
      return new FileObject[0];
    }

    ArrayList<FileObject> classpath = new ArrayList<>();
    for (String path : uris.split(",")) {
      path = path.trim();
      if (path.equals("")) {
        continue;
      }
      FileObject fo = vfs.resolveFile(path);
      switch (fo.getType()) {
        case FILE:
        case FOLDER:
          classpath.add(fo);
          break;
        case IMAGINARY:
          // assume its a pattern
          String pattern = fo.getName().getBaseName();
          if (fo.getParent() != null && fo.getParent().getType() == FileType.FOLDER) {
            FileObject[] children = fo.getParent().getChildren();
            for (FileObject child : children) {
              if (child.getType() == FileType.FILE && child.getName().getBaseName().matches(pattern)) {
                classpath.add(child);
              }
            }
          } else {
            LOG.warn("ignoring classpath entry {}", fo);
          }
          break;
        default:
          LOG.warn("ignoring classpath entry {}", fo);
          break;
      }
    }
    return classpath.toArray(new FileObject[classpath.size()]);
  }
}
