/*
 * Copyright 2012-present Facebook, 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.facebook.buck.rules;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import com.facebook.buck.util.collect.ArrayIterable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.eventbus.Subscribe;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;
import java.util.Comparator;
import java.util.logging.Level;
import java.util.logging.Logger;

public class DirArtifactCache implements ArtifactCache {
  private class FileAccessedEntry {
    public final File file;
    public final FileTime lastAccessTime;

    public File getFile() {
      return file;
    }

    public FileTime getLastAccessTime() {
      return lastAccessTime;
    }

    private FileAccessedEntry(File file, FileTime lastAccessTime) {
      this.file = file;
      this.lastAccessTime = lastAccessTime;
    }
  }


  private final static Logger logger = Logger.getLogger(DirArtifactCache.class.getName());

  /**
   * Sorts by the lastAccessTime in descending order (more recently accessed files are first).
   */
  private final static Comparator<FileAccessedEntry> SORT_BY_LAST_ACCESSED_TIME_DESC =
      new Comparator<FileAccessedEntry>() {
    @Override
    public int compare(FileAccessedEntry a, FileAccessedEntry b) {
      return b.getLastAccessTime().compareTo(a.getLastAccessTime());
    }
  };

  private final File cacheDir;
  private final Optional<Long> maxCacheSizeBytes;

  public DirArtifactCache(File cacheDir, Optional<Long> maxCacheSizeBytes) throws IOException {
    this.cacheDir = Preconditions.checkNotNull(cacheDir);
    this.maxCacheSizeBytes = Preconditions.checkNotNull(maxCacheSizeBytes);
    Files.createDirectories(cacheDir.toPath());
  }

  @Override
  public CacheResult fetch(RuleKey ruleKey, File output) {
    CacheResult success = CacheResult.MISS;
    File cacheEntry = new File(cacheDir, ruleKey.toString());
    if (cacheEntry.exists()) {
      try {
        Files.createDirectories(output.toPath().getParent());
        Files.copy(cacheEntry.toPath(), output.toPath(), REPLACE_EXISTING);
        success = CacheResult.DIR_HIT;
      } catch (IOException e) {
        logger.warning(String.format("Artifact fetch(%s, %s) error: %s",
            ruleKey,
            output.getPath(),
            e.getMessage()));
      }
    }
    logger.info(String.format("Artifact fetch(%s, %s) cache %s",
        ruleKey,
        output.getPath(),
        (success.isSuccess() ? "hit" : "miss")));
    return success;
  }

  @Override
  public void store(RuleKey ruleKey, File output) {
    File cacheEntry = new File(cacheDir, ruleKey.toString());
    Path tmpCacheEntry = null;
    try {
      // Write to a temporary file and move the file to its final location atomically to protect
      // against partial artifacts (whether due to buck interruption or filesystem failure) posing
      // as valid artifacts during subsequent buck runs.
      tmpCacheEntry = File.createTempFile(ruleKey.toString(), ".tmp", cacheDir).toPath();
      Files.copy(output.toPath(), tmpCacheEntry, REPLACE_EXISTING);
      Files.move(tmpCacheEntry, cacheEntry.toPath());
    } catch (IOException e) {
      logger.warning(String.format("Artifact store(%s, %s) error: %s",
          ruleKey,
          output.getPath(),
          e.getMessage()));
      if (tmpCacheEntry != null) {
        try {
          Files.deleteIfExists(tmpCacheEntry);
        } catch (IOException ignored) {
          // Unable to delete a temporary file. Nothing sane to do.
          logger.log(Level.INFO, "Unable to delete temp cache file", ignored);
        }
      }
    }
  }

  /** @return {@code true}: storing artifacts is always supported by this class. */
  @Override
  public boolean isStoreSupported() {
    return true;
  }

  @Subscribe
  public synchronized void buildFinished(BuildEvent.Finished finished) {
    deleteOldFiles();
  }

  /**
   * Deletes files that haven't been accessed recently from the directory cache.
   */
  @VisibleForTesting
  void deleteOldFiles() {
    if (!maxCacheSizeBytes.isPresent()) {
      return;
    }
    for (FileAccessedEntry fileAccessedEntry : findFilesToDelete()) {
      try {
        Files.deleteIfExists(fileAccessedEntry.getFile().toPath());
      } catch (IOException e) {
        // Eat any IOExceptions while attempting to clean up the cache directory.  If the file is
        // now in use, we no longer want to delete it.
        continue;
      }
    }
  }

  private Iterable<FileAccessedEntry> findFilesToDelete() {
    Preconditions.checkState(maxCacheSizeBytes.isPresent());
    long maxSizeBytes = maxCacheSizeBytes.get();

    File[] artifacts = cacheDir.listFiles();
    FileAccessedEntry[] fileAccessedEntries = new FileAccessedEntry[artifacts.length];
    for (int i = 0; i < artifacts.length; ++i) {
      FileTime lastAccess;
      try {
        lastAccess =
            Files.readAttributes(artifacts[i].toPath(), BasicFileAttributes.class).lastAccessTime();
      } catch (IOException e) {
        lastAccess = FileTime.fromMillis(artifacts[i].lastModified());
      }
      fileAccessedEntries[i] = new FileAccessedEntry(artifacts[i], lastAccess);
    }
    Arrays.sort(fileAccessedEntries, SORT_BY_LAST_ACCESSED_TIME_DESC);

    // Finds the first N from the list ordered by last access time who's combined size is less than
    // maxCacheSizeBytes.
    long currentSizeBytes = 0;
    for (int i = 0; i < fileAccessedEntries.length; ++i) {
      FileAccessedEntry file = fileAccessedEntries[i];
      currentSizeBytes += file.getFile().length();
      if (currentSizeBytes > maxSizeBytes) {
        return ArrayIterable.of(fileAccessedEntries, i, fileAccessedEntries.length);
      }
    }
    return ImmutableList.<FileAccessedEntry>of();
  }
}

@@ -4511,16 +4511,21 @@
 %7D%0A%0A  /**
+%0A   *
  @return
@@ -4591,16 +4591,19 @@
 s class.
+%0A  
  */%0A  @O
@@ -4670,16 +4670,86 @@
 e;%0A  %7D%0A%0A
+  /**%0A   * @param finished Signals that the build has finished.%0A   */%0A
   @Subsc

Fix an Eclipse warning about an unused param by documenting it.