/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * 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.intellij.openapi.vcs.changes;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vcs.ConstantZipperUpdater;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.events.*;
import com.intellij.util.Alarm;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

/**
 * Listens to file system events and notifies VcsDirtyScopeManagers responsible for changed files to mark these files dirty.
 */
public class VcsDirtyScopeVfsListener implements BulkFileListener, Disposable {
  @NotNull private final ProjectLevelVcsManager myVcsManager;

  private boolean myForbid; // for tests only

  private final ConstantZipperUpdater myZipperUpdater;
  private final List<FilesAndDirs> myQueue;
  private final Object myLock;
  private final Runnable myDirtReporter;

  public VcsDirtyScopeVfsListener(@NotNull Project project,
                                  @NotNull ProjectLevelVcsManager vcsManager,
                                  @NotNull VcsDirtyScopeManager dirtyScopeManager) {
    myVcsManager = vcsManager;

    myLock = new Object();
    myQueue = new ArrayList<>();
    myDirtReporter = new Runnable() {
      @Override
      public void run() {
        ArrayList<FilesAndDirs> list;
        synchronized (myLock) {
          list = new ArrayList<>(myQueue);
          myQueue.clear();
        }

        for (FilesAndDirs filesAndDirs : list) {
          dirtyScopeManager.filePathsDirty(filesAndDirs.dirtyFiles, filesAndDirs.dirtyDirs);
        }
      }
    };
    myZipperUpdater = new ConstantZipperUpdater(300, Alarm.ThreadToUse.POOLED_THREAD, this, myDirtReporter);
    Disposer.register(project, this);
    project.getMessageBus().connect().subscribe(VirtualFileManager.VFS_CHANGES, this);
  }

  public static VcsDirtyScopeVfsListener getInstance(@NotNull Project project) {
    return ServiceManager.getService(project, VcsDirtyScopeVfsListener.class);
  }

  public void setForbid(boolean forbid) {
    assert ApplicationManager.getApplication().isUnitTestMode();
    myForbid = forbid;
  }

  public void flushDirt() {
    myDirtReporter.run();
  }

  @Override
  public void dispose() {
    synchronized (myLock) {
      myQueue.clear();
    }
  }

  @Override
  public void before(@NotNull List<? extends VFileEvent> events) {
    if (myForbid || !myVcsManager.hasAnyMappings()) return;
    final FilesAndDirs dirtyFilesAndDirs = new FilesAndDirs();
    // collect files and directories - sources of events
    for (VFileEvent event : events) {
      if (event instanceof VFileCreateEvent) continue;
      final VirtualFile file = event.getFile();

      if (file == null || !file.isInLocalFileSystem()) {
        continue;
      }

      if (event instanceof VFileDeleteEvent || event instanceof VFileMoveEvent || event instanceof VFilePropertyChangeEvent) {
        dirtyFilesAndDirs.add(file);
      }
    }
    markDirtyOnPooled(dirtyFilesAndDirs);
  }

  @Override
  public void after(@NotNull List<? extends VFileEvent> events) {
    if (myForbid || !myVcsManager.hasAnyMappings()) return;
    final FilesAndDirs dirtyFilesAndDirs = new FilesAndDirs();
    // collect files and directories - sources of events
    for (VFileEvent event : events) {
      if (event instanceof VFileDeleteEvent) continue;

      final VirtualFile file = event.getFile();
      if (file == null || !file.isInLocalFileSystem()) {
        continue;
      }

      if (event instanceof VFileContentChangeEvent || event instanceof VFileCreateEvent || event instanceof VFileMoveEvent) {
        dirtyFilesAndDirs.add(file);
      }
      else if (event instanceof VFileCopyEvent) {
        VFileCopyEvent copyEvent = (VFileCopyEvent)event;
        dirtyFilesAndDirs.add(copyEvent.getNewParent().findChild(copyEvent.getNewChildName()));
      }
      else if (event instanceof VFilePropertyChangeEvent) {
        final VFilePropertyChangeEvent pce = (VFilePropertyChangeEvent)event;
        if (pce.getPropertyName().equals(VirtualFile.PROP_NAME)) {
          // if a file was renamed, then the file is dirty and its parent directory is dirty too;
          // if a directory was renamed, all its children are recursively dirty, the parent dir is also dirty but not recursively.
          dirtyFilesAndDirs.add(file);   // the file is dirty recursively
          dirtyFilesAndDirs.addToFiles(file.getParent()); // directory is dirty alone. if parent is null - is checked in the method
        } else {
          dirtyFilesAndDirs.addToFiles(file);
        }
      }
    }
    markDirtyOnPooled(dirtyFilesAndDirs);
  }

  private void markDirtyOnPooled(@NotNull FilesAndDirs dirtyFilesAndDirs) {
    synchronized (myLock) {
      myQueue.add(dirtyFilesAndDirs);
    }
    myZipperUpdater.request();
  }

  /**
   * Stores VcsDirtyScopeManagers and files and directories which should be marked dirty by them.
   * Files will be marked dirty, directories will be marked recursively dirty, so if you need to mark dirty a directory, but
   * not recursively, you should add it to files.
   */
  private static class FilesAndDirs {
    @NotNull HashSet<FilePath> dirtyFiles = ContainerUtil.newHashSet();
    @NotNull HashSet<FilePath> dirtyDirs = ContainerUtil.newHashSet();

    private void add(@Nullable VirtualFile file, boolean addToFiles) {
      if (file == null) return;
      boolean isDirectory = file.isDirectory();
      FilePath path = VcsUtil.getFilePath(file.getPath(), isDirectory);
      if (addToFiles || !isDirectory) {
        dirtyFiles.add(path);
      }
      else {
        dirtyDirs.add(path);
      }
    }

    private void add(@Nullable VirtualFile file) {
      add(file, false);
    }

    private void addToFiles(@Nullable VirtualFile file) {
      add(file, true);
    }
  }
}

@@ -21,9 +21,9 @@
 -201
-4
+6
  Jet

update copyright