/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.cli;

import alluxio.conf.AlluxioConfiguration;
import alluxio.conf.PropertyKey;
import alluxio.extensions.ExtensionsClassLoader;
import alluxio.util.io.PathUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;

/**
 * The registry of validation tool implementations.
 */
public class ValidationToolRegistry {
  private static final Logger LOG = LoggerFactory.getLogger(ValidationToolRegistry.class);
  private static final String VALIDATION_TOOL_PATTERN = "alluxio-integration-tools-*.jar";

  private AlluxioConfiguration mConf;
  private Map<String, ValidationToolFactory> mFactories;

  /**
   * Creates a new instance of an {@link ValidationToolRegistry}.
   *
   * @param conf the Alluxio configuration
   */
  public ValidationToolRegistry(AlluxioConfiguration conf) {
    mFactories = new HashMap<>();
    mConf = conf;
  }

  /**
   * Refreshes the registry by service loading classes.
   */
  public void refresh() {
    Map<String, ValidationToolFactory> map = new HashMap<>();

    String libDir = PathUtils.concatPath(mConf.getString(PropertyKey.HOME), "lib");
    LOG.info("Loading validation tool jars from {}", libDir);
    List<File> files = new ArrayList<>();
    try (DirectoryStream<Path> stream = Files
        .newDirectoryStream(Paths.get(libDir), VALIDATION_TOOL_PATTERN)) {
      for (Path entry : stream) {
        if (entry.toFile().isFile()) {
          files.add(entry.toFile());
        }
      }
    } catch (IOException e) {
      LOG.warn("Failed to load validation tool libs from {}. error: {}",
          libDir, e.toString());
    }

    // Load the validation tool factory from libraries
    for (File jar : files) {
      try {
        URL extensionURL = jar.toURI().toURL();
        ClassLoader extensionsClassLoader = new ExtensionsClassLoader(new URL[] {extensionURL},
            ClassLoader.getSystemClassLoader());
        for (ValidationToolFactory factory : ServiceLoader
            .load(ValidationToolFactory.class, extensionsClassLoader)) {
          ValidationToolFactory existingFactory = map.get(factory.getType());
          if (existingFactory != null) {
            LOG.warn(
                "Ignoring duplicate validation tool type '{}' found in {}. Existing factory: {}",
                factory.getType(), factory.getClass(), existingFactory.getClass());
          }
          map.put(factory.getType(), factory);
        }
      } catch (Throwable t) {
        LOG.warn("Failed to load validation tool jar {}", jar, t);
      }
    }

    // Load the validation tools from the default classloader
    for (ValidationToolFactory factory : ServiceLoader
        .load(ValidationToolFactory.class, ValidationToolRegistry.class.getClassLoader())) {
      ValidationToolFactory existingFactory = map.get(factory.getType());
      if (existingFactory != null) {
        LOG.warn("Ignoring duplicate validation tool type '{}' found in {}. Existing factory: {}",
            factory.getType(), factory.getClass(), existingFactory.getClass());
      }
      map.put(factory.getType(), factory);
    }

    mFactories = map;
    LOG.info("Registered Factories: " + String.join(",", mFactories.keySet()));
  }

  /**
   * Creates a new instance of {@link ValidationTool}.
   *
   * @param type the validation tool type
   * @param configMap the validation tool configuration tool
   * @return a new validation tool instance
   */
  public ValidationTool create(String type, Map<Object, Object> configMap) {
    Map<String, ValidationToolFactory> map = mFactories;
    ValidationToolFactory factory = map.get(type);
    if (factory == null) {
      throw new IllegalArgumentException(
          String.format("ValidationToolFactory for type '%s' does not exist.", type));
    }

    ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
    try {
      // Use the extension class loader of the factory.
      Thread.currentThread().setContextClassLoader(factory.getClass().getClassLoader());
      return factory.create(configMap);
    } catch (Throwable e) {
      // Catching Throwable rather than Exception to catch service loading errors
      throw new IllegalStateException(
          String.format("Failed to create ValidationTool by factory %s", factory), e);
    } finally {
      Thread.currentThread().setContextClassLoader(previousClassLoader);
    }
  }
}
