// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.vfs;

import com.google.common.annotations.VisibleForTesting;
import com.google.devtools.build.lib.clock.Clock;
import com.google.devtools.build.lib.clock.JavaClock;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import javax.annotation.Nullable;

/**
 * A FileSystem that does not use any JNI and hence, does not require a shared library be present at
 * execution.
 *
 * <p>Note: Blaze profiler tasks are defined on the system call level - thus we do not distinguish
 * (from profiling perspective) between different methods on this class that end up doing stat()
 * system call - they all are associated with the VFS_STAT task.
 */
@ThreadSafe
public class JavaIoFileSystem extends AbstractFileSystemWithCustomStat {
  private static final LinkOption[] NO_LINK_OPTION = new LinkOption[0];
  // This isn't generally safe; we rely on the file system APIs not modifying the array.
  private static final LinkOption[] NOFOLLOW_LINKS_OPTION =
      new LinkOption[] { LinkOption.NOFOLLOW_LINKS };

  private final Clock clock;

  protected static final String ERR_IS_DIRECTORY = " (Is a directory)";
  protected static final String ERR_DIRECTORY_NOT_EMPTY = " (Directory not empty)";
  protected static final String ERR_FILE_EXISTS = " (File exists)";
  protected static final String ERR_NO_SUCH_FILE_OR_DIR = " (No such file or directory)";
  protected static final String ERR_NOT_A_DIRECTORY = " (Not a directory)";

  public JavaIoFileSystem(DigestHashFunction hashFunction) {
    super(hashFunction);
    this.clock = new JavaClock();
  }

  @VisibleForTesting
  JavaIoFileSystem(Clock clock, DigestHashFunction hashFunction) {
    super(hashFunction);
    this.clock = clock;
  }

  protected File getIoFile(PathFragment path) {
    return new File(path.toString());
  }

  /**
   * Returns a {@link java.nio.file.Path} representing the same path as provided {@code path}.
   *
   * <p>Note: while it's possible to use {@link #getIoFile(PathFragment)} in combination with {@link
   * File#toPath()} to achieve essentially the same, using this method is preferable because it
   * avoids extra allocations and does not lose track of the underlying Java filesystem, which is
   * useful for some in-memory filesystem implementations like JimFS.
   */
  protected java.nio.file.Path getNioPath(PathFragment path) {
    return Paths.get(path.toString());
  }

  private LinkOption[] linkOpts(boolean followSymlinks) {
    return followSymlinks ? NO_LINK_OPTION : NOFOLLOW_LINKS_OPTION;
  }

  @Override
  protected Collection<String> getDirectoryEntries(PathFragment path) throws IOException {
    File file = getIoFile(path);
    String[] entries = null;
    long startTime = Profiler.nanoTimeMaybe();
    try {
      entries = file.list();
      if (entries == null) {
        if (file.exists()) {
          throw new IOException(path + ERR_NOT_A_DIRECTORY);
        } else {
          throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
        }
      }
    } finally {
      profiler.logSimpleTask(startTime, ProfilerTask.VFS_DIR, file.getPath());
    }
    Collection<String> result = new ArrayList<>(entries.length);
    for (String entry : entries) {
      if (!entry.equals(".") && !entry.equals("..")) {
        result.add(entry);
      }
    }
    return result;
  }

  @Override
  protected boolean exists(PathFragment path, boolean followSymlinks) {
    long startTime = Profiler.nanoTimeMaybe();
    try {
      java.nio.file.Path nioPath = getNioPath(path);
      return Files.exists(nioPath, linkOpts(followSymlinks));
    } catch (InvalidPathException e) {
      return false;
    } finally {
      profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, path.toString());
    }
  }

  @Override
  protected boolean isReadable(PathFragment path) throws IOException {
    File file = getIoFile(path);
    long startTime = Profiler.nanoTimeMaybe();
    try {
      if (!file.exists()) {
        throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
      }
      return file.canRead();
    } finally {
      profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath());
    }
  }

  @Override
  protected boolean isWritable(PathFragment path) throws IOException {
    File file = getIoFile(path);
    long startTime = Profiler.nanoTimeMaybe();
    try {
      if (!file.exists()) {
        if (linkExists(file)) {
          throw new IOException(path + ERR_PERMISSION_DENIED);
        } else {
          throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
        }
      }
      return file.canWrite();
    } finally {
      profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath());
    }
  }

  @Override
  protected boolean isExecutable(PathFragment path) throws IOException {
    File file = getIoFile(path);
    long startTime = Profiler.nanoTimeMaybe();
    try {
      if (!file.exists()) {
        throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
      }
      return file.canExecute();
    } finally {
      profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath());
    }
  }

  @Override
  protected void setReadable(PathFragment path, boolean readable) throws IOException {
    File file = getIoFile(path);
    if (!file.exists()) {
      throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
    }
    if (!file.setReadable(readable) && readable) {
      throw new IOException(String.format("Failed to make %s readable", path));
    }
  }

  @Override
  public void setWritable(PathFragment path, boolean writable) throws IOException {
    File file = getIoFile(path);
    if (!file.exists()) {
      throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
    }
    if (!file.setWritable(writable) && writable) {
      throw new IOException(String.format("Failed to make %s writable", path));
    }
  }

  @Override
  protected void setExecutable(PathFragment path, boolean executable) throws IOException {
    File file = getIoFile(path);
    if (!file.exists()) {
      throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
    }
    if (!file.setExecutable(executable) && executable) {
      throw new IOException(String.format("Failed to make %s executable", path));
    }
  }

  @Override
  public boolean supportsModifications(PathFragment path) {
    return true;
  }

  @Override
  public boolean supportsSymbolicLinksNatively(PathFragment path) {
    return true;
  }

  @Override
  public boolean supportsHardLinksNatively(PathFragment path) {
    return true;
  }

  @Override
  public boolean isFilePathCaseSensitive() {
    return true;
  }

  @Override
  public boolean createDirectory(PathFragment path) throws IOException {
    File file = getIoFile(path);
    if (file.mkdir()) {
      return true;
    }

    if (fileIsSymbolicLink(file)) {
      throw new IOException(path + ERR_FILE_EXISTS);
    }
    if (file.isDirectory()) {
      return false; // directory already existed
    } else if (file.exists()) {
      throw new IOException(path + ERR_FILE_EXISTS);
    } else if (!file.getParentFile().exists()) {
      throw new FileNotFoundException(path.getParentDirectory() + ERR_NO_SUCH_FILE_OR_DIR);
    }
    // Parent directory apparently exists - try to create our directory again.
    if (file.mkdir()) {
      return true; // Everything is fine finally.
    } else if (!file.getParentFile().canWrite()) {
      throw new FileAccessException(path + ERR_PERMISSION_DENIED);
    } else {
      // Parent exists, is writable, yet we can't create our directory.
      throw new FileNotFoundException(path.getParentDirectory() + ERR_NOT_A_DIRECTORY);
    }
  }

  @Override
  public void createDirectoryAndParents(PathFragment path) throws IOException {
    java.nio.file.Path nioPath = getNioPath(path);
    try {
      Files.createDirectories(nioPath);
    } catch (java.nio.file.FileAlreadyExistsException e) {
      // Files.createDirectories will handle this case normally, but if the existing
      // file is a symlink to a directory then it still throws. Swallow this.
      if (!isDirectory(path, /*followSymlinks=*/ true)) {
        throw e;
      }
    }
  }

  private boolean linkExists(File file) {
    String shortName = file.getName();
    File parentFile = file.getParentFile();
    if (parentFile == null) {
      return false;
    }
    String[] filenames = parentFile.list();
    if (filenames == null) {
      return false;
    }
    for (String name : filenames) {
      if (name.equals(shortName)) {
        return true;
      }
    }
    return false;
  }

  @Override
  protected void createSymbolicLink(PathFragment linkPath, PathFragment targetFragment)
      throws IOException {
    java.nio.file.Path nioPath = getNioPath(linkPath);
    try {
      Files.createSymbolicLink(nioPath, Paths.get(targetFragment.getSafePathString()));
    } catch (java.nio.file.FileAlreadyExistsException e) {
      throw new IOException(linkPath + ERR_FILE_EXISTS, e);
    } catch (java.nio.file.AccessDeniedException e) {
      throw new IOException(linkPath + ERR_PERMISSION_DENIED, e);
    } catch (java.nio.file.NoSuchFileException e) {
      throw new FileNotFoundException(linkPath + ERR_NO_SUCH_FILE_OR_DIR);
    }
  }

  @Override
  protected PathFragment readSymbolicLink(PathFragment path) throws IOException {
    java.nio.file.Path nioPath = getNioPath(path);
    long startTime = Profiler.nanoTimeMaybe();
    try {
      String link = Files.readSymbolicLink(nioPath).toString();
      return PathFragment.create(link);
    } catch (java.nio.file.NotLinkException e) {
      throw new NotASymlinkException(path, e);
    } catch (java.nio.file.NoSuchFileException e) {
      throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
    } finally {
      profiler.logSimpleTask(startTime, ProfilerTask.VFS_READLINK, path.getPathString());
    }
  }

  @Override
  public void renameTo(PathFragment sourcePath, PathFragment targetPath) throws IOException {
    File sourceFile = getIoFile(sourcePath);
    File targetFile = getIoFile(targetPath);
    if (!sourceFile.renameTo(targetFile)) {
      if (!sourceFile.exists()) {
        throw new FileNotFoundException(sourcePath + ERR_NO_SUCH_FILE_OR_DIR);
      }
      if (targetFile.exists()) {
        if (targetFile.isDirectory() && targetFile.list().length > 0) {
          throw new IOException(targetPath + ERR_DIRECTORY_NOT_EMPTY);
        } else if (sourceFile.isDirectory() && targetFile.isFile()) {
          throw new IOException(sourcePath + " -> " + targetPath + ERR_NOT_A_DIRECTORY);
        } else if (sourceFile.isFile() && targetFile.isDirectory()) {
          throw new IOException(sourcePath + " -> " + targetPath + ERR_IS_DIRECTORY);
        } else {
          throw new IOException(sourcePath + " -> " + targetPath  + ERR_PERMISSION_DENIED);
        }
      } else {
        throw new FileAccessException(sourcePath + " -> " + targetPath + ERR_PERMISSION_DENIED);
      }
    }
  }

  @Override
  protected long getFileSize(PathFragment path, boolean followSymlinks) throws IOException {
    long startTime = Profiler.nanoTimeMaybe();
    try {
      return stat(path, followSymlinks).getSize();
    } finally {
      profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, path.getPathString());
    }
  }

  @Override
  protected boolean delete(PathFragment path) throws IOException {
    java.nio.file.Path nioPath = getNioPath(path);
    long startTime = Profiler.nanoTimeMaybe();
    try {
      return Files.deleteIfExists(nioPath);
    } catch (java.nio.file.DirectoryNotEmptyException e) {
      throw new IOException(path.getPathString() + ERR_DIRECTORY_NOT_EMPTY, e);
    } catch (java.nio.file.AccessDeniedException e) {
      throw new IOException(path.getPathString() + ERR_PERMISSION_DENIED, e);
    } catch (java.nio.file.AtomicMoveNotSupportedException
        | java.nio.file.FileAlreadyExistsException
        | java.nio.file.FileSystemLoopException
        | java.nio.file.NoSuchFileException
        | java.nio.file.NotDirectoryException
        | java.nio.file.NotLinkException e) {
      // All known but unexpected subclasses of FileSystemException.
      throw new IOException(path.getPathString() + ": unexpected FileSystemException", e);
    } catch (java.nio.file.FileSystemException e) {
      // Files.deleteIfExists() throws FileSystemException on Linux if a path component is a file.
      // We caught all known subclasses of FileSystemException so `e` is either an unknown
      // subclass or it is indeed a "Not a directory" error. Non-English JDKs may use a different
      // error message than "Not a directory", so we should not look for that text. Checking the
      // parent directory if it's indeed a directory is unrealiable, because another process may
      // modify it concurrently... but we have no better choice.
      if (e.getClass().equals(java.nio.file.FileSystemException.class)
          && !nioPath.getParent().toFile().isDirectory()) {
        // Hopefully the try-block failed because a parent directory was in fact not a directory.
        // Theoretically it's possible that the try-block failed for some other reason and all
        // parent directories were indeed directories, but another process changed a parent
        // directory into a file after the try-block failed but before this catch-block started, and
        // we return false here losing the real exception in `e`, but we cannot know.
        return false;
      } else {
        throw new IOException(path.getPathString() + ": unexpected FileSystemException", e);
      }
    } finally {
      profiler.logSimpleTask(startTime, ProfilerTask.VFS_DELETE, path.getPathString());
    }
  }

  @Override
  protected long getLastModifiedTime(PathFragment path, boolean followSymlinks) throws IOException {
    File file = getIoFile(path);
    long startTime = Profiler.nanoTimeMaybe();
    try {
      return stat(path, followSymlinks).getLastModifiedTime();
    } finally {
      profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath());
    }
  }

  protected boolean fileIsSymbolicLink(File file) {
    return Files.isSymbolicLink(file.toPath());
  }

  @Override
  public void setLastModifiedTime(PathFragment path, long newTime) throws IOException {
    File file = getIoFile(path);
    if (!file.setLastModified(
        newTime == Path.NOW_SENTINEL_TIME ? clock.currentTimeMillis() : newTime)) {
      if (!file.exists()) {
        throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
      } else if (!file.getParentFile().canWrite()) {
        throw new FileAccessException(path.getParentDirectory() + ERR_PERMISSION_DENIED);
      } else {
        throw new FileAccessException(path + ERR_PERMISSION_DENIED);
      }
    }
  }

  @Override
  protected byte[] getDigest(PathFragment path) throws IOException {
    String name = path.toString();
    long startTime = Profiler.nanoTimeMaybe();
    try {
      return super.getDigest(path);
    } finally {
      profiler.logSimpleTask(startTime, ProfilerTask.VFS_MD5, name);
    }
  }

  /**
   * Returns the status of a file. See {@link Path#stat(Symlinks)} for specification.
   *
   * <p>The default implementation of this method is a "lazy" one, based on other accessor methods
   * such as {@link #isFile}, etc. Subclasses may provide more efficient specializations. However,
   * we still try to follow Unix-like semantics of failing fast in case of non-existent files (or in
   * case of permission issues).
   */
  @Override
  protected FileStatus stat(PathFragment path, boolean followSymlinks) throws IOException {
    java.nio.file.Path nioPath = getNioPath(path);
    final BasicFileAttributes attributes;
    try {
      attributes =
          Files.readAttributes(nioPath, BasicFileAttributes.class, linkOpts(followSymlinks));
    } catch (java.nio.file.FileSystemException e) {
      throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
    }
    FileStatus status =
        new FileStatus() {
          @Override
          public boolean isFile() {
            return attributes.isRegularFile() || isSpecialFile();
          }

          @Override
          public boolean isSpecialFile() {
            return attributes.isOther();
          }

          @Override
          public boolean isDirectory() {
            return attributes.isDirectory();
          }

          @Override
          public boolean isSymbolicLink() {
            return attributes.isSymbolicLink();
          }

          @Override
          public long getSize() {
            return attributes.size();
          }

          @Override
          public long getLastModifiedTime() {
            return attributes.lastModifiedTime().toMillis();
          }

          @Override
          public long getLastChangeTime() {
            // This is the best we can do with Java NIO...
            return attributes.lastModifiedTime().toMillis();
          }

          @Override
          public long getNodeId() {
            // TODO(bazel-team): Consider making use of attributes.fileKey().
            return -1;
          }
        };

    return status;
  }

  @Override
  @Nullable
  protected FileStatus statIfFound(PathFragment path, boolean followSymlinks) {
    try {
      return stat(path, followSymlinks);
    } catch (FileNotFoundException e) {
      // JavaIoFileSystem#stat (incorrectly) only throws FileNotFoundException (because it calls
      // #getLastModifiedTime, which can only throw a FileNotFoundException), so we always hit this
      // codepath. Thus, this method will incorrectly not throw an exception for some filesystem
      // errors.
      return null;
    } catch (IOException e) {
      // If this codepath is ever hit, then this method should be rewritten to properly distinguish
      // between not-found exceptions and others.
      throw new IllegalStateException(e);
    }
  }

  @Override
  protected void createFSDependentHardLink(PathFragment linkPath, PathFragment originalPath)
      throws IOException {
    Files.createLink(
        java.nio.file.Paths.get(linkPath.toString()),
        java.nio.file.Paths.get(originalPath.toString()));
  }
}
