// 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.util;

import com.google.devtools.build.lib.clock.BlazeClock;
import com.google.devtools.build.lib.testutil.TestUtils;
import com.google.devtools.build.lib.util.StringUtilities;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * Base class for a testing apparatus for a scratch filesystem.
 */
public class FsApparatus {

  /* ---------- State that the apparatus initializes / operates on --------- */
  protected FileSystem fileSystem = null;
  protected Path workingDir = null;

  public static FsApparatus newInMemory() {
    return new FsApparatus();
  }

  // TestUtil.getTmpDir is slow, so cache the result here
  private static final String TMP_DIR =
      new File(TestUtils.tmpDir(), "bs").toString();


  /**
   * When using a Native file system, absolute paths will be treated as absolute paths on the unix
   * file system, as opposed to paths relative to the backing temp directory. So for simplicity,
   * you ought to only use relative paths for FsApparatus#file, FsApparatus#dir, and
   * FsApparatus#path. Otherwise, be aware of the following issue
   *
   *   Path p1 = scratch.path(...);
   *   Path p2 = scratch.path(p1.getPathString());
   *
   * We'd like the invariant that p1.equals(p2) regardless if scratch is in-memory or not, but this
   * does not hold with our usage of Unix filesystems.
   */
  public static FsApparatus newNative() {
    FileSystem fs = FileSystems.getNativeFileSystem();
    Path wd = fs.getPath(TMP_DIR);

    try {
      FileSystemUtils.deleteTree(wd);
    } catch (IOException e) {
      throw new AssertionError(e.getMessage());
    }

    return new FsApparatus(fs, wd);
  }

  private FsApparatus() {
    fileSystem = new InMemoryFileSystem(BlazeClock.instance());
    workingDir = fileSystem.getPath("/");
  }

  public FsApparatus(FileSystem fs, Path cwd) {
    fileSystem = fs;
    workingDir = cwd;
  }

  public FsApparatus(FileSystem fs) {
    fileSystem = fs;
    workingDir = fs.getPath("/");
  }

  public FileSystem fs() {
    return fileSystem;
  }

  /**
   * Creates a scratch file in the scratch filesystem with the given {@code pathName} with
   * {@code lines} being its content. The method returns a Path instance for the scratch file.
   */
  public Path file(String pathName, String... lines) throws IOException {
    Path file = path(pathName);
    Path parentDir = file.getParentDirectory();
    if (!parentDir.exists()) {
      FileSystemUtils.createDirectoryAndParents(parentDir);
    }
    if (file.exists()) {
      throw new IOException("Could not create scratch file (file exists) "
          + file);
    }
    String fileContent = StringUtilities.joinLines(lines);
    FileSystemUtils.writeContentAsLatin1(file, fileContent);
    return file;
  }

  /**
   * Creates or recreates a scratch file just like {@link #file} but tolerating an existing file.
   */
  public Path overwriteFile(String pathName, String... lines) throws IOException {
    try {
      path(pathName).delete();
    } catch (FileNotFoundException e) {
      // Ignored.
    }
    return file(pathName, lines);
  }

  /**
   * Initializes this apparatus (if it hasn't been initialized yet), and creates
   * a directory in the scratch filesystem, with the given {@code pathName}.
   * Creates parent directories as necessary.
   */
  public Path dir(String pathName) throws IOException {
    Path dir = path(pathName);
    if (!dir.exists()) {
      FileSystemUtils.createDirectoryAndParents(dir);
    }
    if (!dir.isDirectory()) {
      throw new IOException("Exists, but is not a directory: " + dir);
    }
    return dir;
  }

  /**
   * Resolves {@code pathName} relative to the working directory. Note that this will not create any
   * entity in the filesystem; i.e., the file that the object is describing may not exist in the
   * filesystem.
   */
  public Path path(String pathName) {
    return workingDir.getRelative(pathName);
  }

  /**
   * Create a fresh directory in the system temporary directory, instead of the
   * testing directory provided by the testing framework. This path is usually
   * shorter than a path starting with TestUtil.getTmpDir(). We care about the
   * length because of the path length restriction for Unix local socket files.
   *
   * Clients are responsible for deleting the directory after tests.
   */
  public Path createUnixTempDir() throws IOException {
    if (fileSystem instanceof InMemoryFileSystem) {
      throw new IOException("Can not create Unix temporary directories in "
                            + "an in-memory file system");
    }
    File file = File.createTempFile("scratch", "tmp");
    final Path path = fileSystem.getPath(file.getAbsolutePath());
    path.delete();
    path.createDirectory();
    return path;
  }
}
