/*
 * Copyright (C) 2016 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.copybara.git;

import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.copybara.exception.RepoException;
import com.google.copybara.util.console.Console;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * A walker which adds all files not matching a glob to the index of a Git repo using {@code git
 * add}.
 */
final class AddExcludedFilesToIndex {
  private final GitRepository repo;
  private final PathMatcher pathMatcher;
  private ArrayList<String> addBackSubmodules;

  AddExcludedFilesToIndex(GitRepository repo, PathMatcher pathMatcher) {
    this.repo = Preconditions.checkNotNull(repo);
    this.pathMatcher = Preconditions.checkNotNull(pathMatcher);
  }

  /**
   * Finds and records the path of all submodules. This should be called when they are not staged
   * for deletion.
   */
  void findSubmodules(Console console) throws RepoException {
    addBackSubmodules = new ArrayList<>();

    String submoduleStatus = repo.simpleCommand("submodule", "status").getStdout();
    for (String line : Splitter.on('\n').omitEmptyStrings().split(submoduleStatus)) {
      String submoduleName = line.replaceFirst("^-[0-9a-f]{40} ", "");
      if (submoduleName.equals(line)) {
        console.warn("Cannot parse line from 'git submodule status': " + line);
        continue;
      }
      if (!pathMatcher.matches(repo.getWorkTree().resolve(submoduleName))) {
        addBackSubmodules.add(submoduleName);
      }
    }
  }

  /**
   * Adds all the excluded files and submodules.
   */
  void add() throws RepoException, IOException {
    ExcludesFinder visitor = new ExcludesFinder(repo.getGitDir(), pathMatcher, repo.getWorkTree());
    Files.walkFileTree(repo.getWorkTree(), visitor);

    int size = 0;
    List<String> current = new ArrayList<>();
    for (String path : visitor.excludedFiles.values()) {
      current.add(path);
      size += path.length();
      // Split the executions in chunks of 6K. 8K triggers arg max in some systems, so
      // this is a reasonable number to get some batching benefit.
      if (size > 6 * 1024) {
        repo.add().force().files(current).run();
        current = new ArrayList<>();
        size = 0;
      }
    }
    if (!current.isEmpty()) {
      repo.add().force().files(current).run();
    }

    for (String addBackSubmodule : addBackSubmodules) {
      repo.simpleCommand("reset", "--", "--quiet", addBackSubmodule);
      repo.add().force().files(ImmutableList.of(addBackSubmodule)).run();
    }
  }

  private static final class ExcludesFinder extends SimpleFileVisitor<Path> {

    private final Path gitDir;
    private final PathMatcher destinationFiles;
    private final Path root;
    private final SetMultimap<Path, String> excludedFiles = HashMultimap.create();
    private final Set<Path> someMatchInDir = Sets.newHashSet();

    private ExcludesFinder(Path gitDir, PathMatcher destinationFiles, Path root) {
      this.gitDir = gitDir;
      this.destinationFiles = destinationFiles;
      this.root = root;
    }

    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
        throws IOException {
      if (dir.equals(gitDir)) {
        return FileVisitResult.SKIP_SUBTREE;
      }
      return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
      if (destinationFiles.matches(file)) {
        // Tell parent dir that one of its children is affected
        someMatchInDir.add(file.getParent());
      } else {
        excludedFiles.put(file.getParent(), file.toString());
        if (Files.isHidden(file)) {
          // Dir is not affected but 'git add dir' doesn't work for 'dir/.file'.
          someMatchInDir.add(file.getParent());
        }
      }
      return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
      if (someMatchInDir.contains(dir)) {
        // Tell parent dir that one of its children is affected
        someMatchInDir.add(dir.getParent());
      } else if (!root.equals(dir)) {
        // If everything inside the dir is affected, replace it with the folder itself
        excludedFiles.removeAll(dir);
        excludedFiles.put(dir.getParent(), dir.toString());
      }
      return super.postVisitDirectory(dir, exc);
    }
  }
}

@@ -745,153 +745,21 @@
 ect.
-HashMultimap;%0Aimport com.google.common.collect.ImmutableList;%0Aimport com.google.common.collect.SetMultimap;%0Aimport com.google.common.collect.Sets
+ImmutableList
 ;%0Aim
@@ -1158,30 +1158,8 @@
 ist;
-%0Aimport java.util.Set;
 %0A%0A/*
@@ -2580,28 +2580,8 @@
 cher
-, repo.getWorkTree()
 );%0A 
@@ -2740,22 +2740,8 @@
 uded
-Files.values()
 ) %7B%0A
@@ -3570,162 +3570,47 @@
 nal 
-Path root;%0A    private final SetMultimap%3CPath, String%3E excludedFiles = HashMultimap.create();%0A    private final Set%3CPath%3E someMatchInDir = Sets.newHashSet
+List%3CString%3E excluded = new ArrayList%3C%3E
 ();%0A
@@ -3682,19 +3682,8 @@
 iles
-, Path root
 ) %7B%0A
@@ -3762,32 +3762,8 @@
 es;%0A
-      this.root = root;%0A
     
@@ -4141,16 +4141,17 @@
     if (
+!
 destinat
@@ -4188,960 +4188,92 @@
     
-// Tell parent dir that one of its children is affected%0A        someMatchInDir.add(file.getParent());%0A      %7D else %7B%0A        excludedFiles.put(file.getParent(), file.toString());%0A        if (Files.isHidden(file)) %7B%0A          // Dir is not affected but 'git add dir' doesn't work for 'dir/.file'.%0A          someMatchInDir.add(file.getParent());%0A        %7D%0A      %7D%0A      return FileVisitResult.CONTINUE;%0A    %7D%0A%0A    @Override%0A    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException %7B%0A      if (someMatchInDir.contains(dir)) %7B%0A        // Tell parent dir that one of its children is affected%0A        someMatchInDir.add(dir.getParent());%0A      %7D else if (!root.equals(dir)) %7B%0A        // If everything inside the dir is affected, replace it with the folder itself%0A        excludedFiles.removeAll(dir);%0A        excludedFiles.put(dir.getParent(), dir.toString());%0A      %7D%0A      return super.postVisitDirectory(dir, exc);%0A    %7D
+excluded.add(file.toString());%0A      %7D%0A      return FileVisitResult.CONTINUE;%0A    %7D%0A
 %0A  %7D

Revert "Exclude directories instead of individual files"