/*
 * Copyright 2013 Google Inc.
 *
 * 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.
 */

package com.google.common.jimfs;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.jimfs.SystemJimfsFileSystemProvider.FILE_SYSTEM_KEY;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.ProviderNotFoundException;
import java.nio.file.spi.FileSystemProvider;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * Static factory methods for creating new Jimfs file systems. File systems may either be created
 * with a basic configuration matching the current operating system or by providing a specific
 * {@link Configuration}. Basic {@linkplain Configuration#unix() UNIX}, {@linkplain
 * Configuration#osX() Mac OS X} and {@linkplain Configuration#windows() Windows} configurations are
 * provided.
 *
 * <p>Examples:
 *
 * <pre>
 *   // A file system with a configuration similar to the current OS
 *   FileSystem fileSystem = Jimfs.newFileSystem();
 *
 *   // A file system with paths and behavior generally matching that of Windows
 *   FileSystem windows = Jimfs.newFileSystem(Configuration.windows());  </pre>
 *
 * <p>Additionally, various behavior of the file system can be customized by creating a custom
 * {@link Configuration}. A modified version of one of the existing default configurations can be
 * created using {@link Configuration#toBuilder()} or a new configuration can be created from
 * scratch with {@link Configuration#builder(PathType)}. See {@link Configuration.Builder} for what
 * can be configured.
 *
 * <p>Examples:
 *
 * <pre>
 *   // Modify the default UNIX configuration
 *   FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix()
 *       .toBuilder()
 *       .setAttributeViews("basic", "owner", "posix", "unix")
 *       .setWorkingDirectory("/home/user")
 *       .setBlockSize(4096)
 *       .build());
 *
 *   // Create a custom configuration
 *   Configuration config = Configuration.builder(PathType.windows())
 *       .setRoots("C:\\", "D:\\", "E:\\")
 *       // ...
 *       .build();  </pre>
 *
 * @author Colin Decker
 */
public final class Jimfs {

  /** The URI scheme for the Jimfs file system ("jimfs"). */
  public static final String URI_SCHEME = "jimfs";

  private static final Logger LOGGER = Logger.getLogger(Jimfs.class.getName());

  private Jimfs() {}

  /**
   * Creates a new in-memory file system with a {@linkplain Configuration#forCurrentPlatform()
   * default configuration} appropriate to the current operating system.
   *
   * <p>More specifically, if the operating system is Windows, {@link Configuration#windows()} is
   * used; if the operating system is Mac OS X, {@link Configuration#osX()} is used; otherwise,
   * {@link Configuration#unix()} is used.
   */
  public static FileSystem newFileSystem() {
    return newFileSystem(newRandomFileSystemName());
  }

  /**
   * Creates a new in-memory file system with a {@linkplain Configuration#forCurrentPlatform()
   * default configuration} appropriate to the current operating system.
   *
   * <p>More specifically, if the operating system is Windows, {@link Configuration#windows()} is
   * used; if the operating system is Mac OS X, {@link Configuration#osX()} is used; otherwise,
   * {@link Configuration#unix()} is used.
   *
   * <p>The returned file system uses the given name as the host part of its URI and the URIs of
   * paths in the file system. For example, given the name {@code my-file-system}, the file system's
   * URI will be {@code jimfs://my-file-system} and the URI of the path {@code /foo/bar} will be
   * {@code jimfs://my-file-system/foo/bar}.
   */
  public static FileSystem newFileSystem(String name) {
    return newFileSystem(name, Configuration.forCurrentPlatform());
  }

  /** Creates a new in-memory file system with the given configuration. */
  public static FileSystem newFileSystem(Configuration configuration) {
    return newFileSystem(newRandomFileSystemName(), configuration);
  }

  /**
   * Creates a new in-memory file system with the given configuration.
   *
   * <p>The returned file system uses the given name as the host part of its URI and the URIs of
   * paths in the file system. For example, given the name {@code my-file-system}, the file system's
   * URI will be {@code jimfs://my-file-system} and the URI of the path {@code /foo/bar} will be
   * {@code jimfs://my-file-system/foo/bar}.
   */
  public static FileSystem newFileSystem(String name, Configuration configuration) {
    try {
      URI uri = new URI(URI_SCHEME, name, null, null);
      return newFileSystem(uri, configuration);
    } catch (URISyntaxException e) {
      throw new IllegalArgumentException(e);
    }
  }

  @VisibleForTesting
  static FileSystem newFileSystem(URI uri, Configuration config) {
    checkArgument(
        URI_SCHEME.equals(uri.getScheme()), "uri (%s) must have scheme %s", uri, URI_SCHEME);

    try {
      // Create the FileSystem. It uses JimfsFileSystemProvider as its provider, as that is
      // the provider that actually implements the operations needed for Files methods to work.
      JimfsFileSystem fileSystem =
          JimfsFileSystems.newFileSystem(JimfsFileSystemProvider.instance(), uri, config);

      /*
       * Now, call FileSystems.newFileSystem, passing it the FileSystem we just created. This
       * allows the system-loaded SystemJimfsFileSystemProvider instance to cache the FileSystem
       * so that methods like Paths.get(URI) work.
       * We do it in this awkward way to avoid issues when the classes in the API (this class
       * and Configuration, for example) are loaded by a different classloader than the one that
       * loads SystemJimfsFileSystemProvider using ServiceLoader. See
       * https://github.com/google/jimfs/issues/18 for gory details.
       */
      try {
        ImmutableMap<String, ?> env = ImmutableMap.of(FILE_SYSTEM_KEY, fileSystem);
        FileSystems.newFileSystem(uri, env, SystemJimfsFileSystemProvider.class.getClassLoader());
      } catch (ProviderNotFoundException | ServiceConfigurationError ignore) {
        // See the similar catch block below for why we ignore this.
        // We log there rather than here so that there's only typically one such message per VM.
      }

      return fileSystem;
    } catch (IOException e) {
      throw new AssertionError(e);
    }
  }

  /**
   * The system-loaded instance of {@code SystemJimfsFileSystemProvider}, or {@code null} if it
   * could not be found or loaded.
   */
  static final @Nullable FileSystemProvider systemProvider = getSystemJimfsProvider();

  /**
   * Returns the system-loaded instance of {@code SystemJimfsFileSystemProvider} or {@code null} if
   * it could not be found or loaded.
   *
   * <p>Like {@link FileSystems#newFileSystem(URI, Map, ClassLoader)}, this method first looks in
   * the list of {@linkplain FileSystemProvider#installedProviders() installed providers} and if not
   * found there, attempts to load it from the {@code ClassLoader} with {@link ServiceLoader}.
   *
   * <p>The idea is that this method should return an instance of the same class (i.e. loaded by the
   * same class loader) as the class whose static cache a {@code JimfsFileSystem} instance will be
   * placed in when {@code FileSystems.newFileSystem} is called in {@code Jimfs.newFileSystem}.
   */
  private static @Nullable FileSystemProvider getSystemJimfsProvider() {
    try {
      for (FileSystemProvider provider : FileSystemProvider.installedProviders()) {
        if (provider.getScheme().equals(URI_SCHEME)) {
          return provider;
        }
      }

      /*
       * Jimfs.newFileSystem passes SystemJimfsFileSystemProvider.class.getClassLoader() to
       * FileSystems.newFileSystem so that it will fall back to loading from that classloader if
       * the provider isn't found in the installed providers. So do the same fallback here to ensure
       * that we can remove file systems from the static cache on SystemJimfsFileSystemProvider if
       * it gets loaded that way.
       */
      ServiceLoader<FileSystemProvider> loader =
          ServiceLoader.load(
              FileSystemProvider.class, SystemJimfsFileSystemProvider.class.getClassLoader());
      for (FileSystemProvider provider : loader) {
        if (provider.getScheme().equals(URI_SCHEME)) {
          return provider;
        }
      }
    } catch (ProviderNotFoundException | ServiceConfigurationError e) {
      /*
       * This can apparently (https://github.com/google/jimfs/issues/31) occur in an environment
       * where services are not loaded from META-INF/services, such as JBoss/Wildfly. In this
       * case, FileSystems.newFileSystem will most likely fail in the same way when called from
       * Jimfs.newFileSystem above, and there will be no way to make URI-based methods like
       * Paths.get(URI) work. Rather than making the user completly unable to use Jimfs, just
       * log this exception and continue.
       *
       * Note: Catching both ProviderNotFoundException, which would occur if no provider matching
       * the "jimfs" URI scheme is found, and ServiceConfigurationError, which can occur if the
       * ServiceLoader finds the META-INF/services entry for Jimfs (or some other
       * FileSystemProvider!) but is then unable to load that class.
       */
      LOGGER.log(
          Level.INFO,
          "An exception occurred when attempting to find the system-loaded FileSystemProvider "
              + "for Jimfs. This likely means that your environment does not support loading "
              + "services via ServiceLoader or is not configured correctly. This does not prevent "
              + "using Jimfs, but it will mean that methods that look up via URI such as "
              + "Paths.get(URI) cannot work.",
          e);
    }

    return null;
  }

  private static String newRandomFileSystemName() {
    return UUID.randomUUID().toString();
  }
}
