/*
 * 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.vfs.ex.dummy;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.project.ProjectManagerAdapter;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.BidirectionalMap;
import com.intellij.util.containers.ConcurrentFactoryMap;
import com.intellij.util.containers.FactoryMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import java.io.IOException;
import java.util.Collection;
import java.util.List;

/**
 * @author gregsh
 */
public abstract class DummyCachingFileSystem<T extends VirtualFile> extends DummyFileSystem {
  private static final Logger LOG = Logger.getInstance("com.intellij.openapi.vfs.ex.dummy.DummyCachingFileSystem");

  private final String myProtocol;

  private final BidirectionalMap<Project, String> myProject2Id = new BidirectionalMap<>();

  private final FactoryMap<String, T> myCachedFiles = new ConcurrentFactoryMap<String, T>() {
    @Override
    protected T create(String key) {
      if (ApplicationManager.getApplication().isUnitTestMode()) {
        //noinspection TestOnlyProblems
        cleanup();
        initProjectMap();
      }
      return findFileByPathInner(key);
    }

    @Override
    public T get(Object key) {
      T file = super.get(key);
      if (file != null && !file.isValid()) {
        remove(key);
        return super.get(key);
      }
      return file;
    }
  };

  public DummyCachingFileSystem(String protocol) {
    myProtocol = protocol;

    final Application application = ApplicationManager.getApplication();
    application.getMessageBus().connect(application).subscribe(ProjectManager.TOPIC, new ProjectManagerAdapter() {
      @Override
      public void projectOpened(final Project project) {
        onProjectOpened(project);
      }
    });
    initProjectMap();
  }

  @NotNull
  @Override
  public final String getProtocol() {
    return myProtocol;
  }

  @Override
  @Nullable
  public final VirtualFile createRoot(String name) {
    return null;
  }

  @Override
  public final T findFileByPath(@NotNull String path) {
    return myCachedFiles.get(path);
  }

  @Override
  @NotNull
  public String extractPresentableUrl(@NotNull String path) {
    VirtualFile file = findFileByPath(path);
    return file != null ? getPresentableUrl(file) : super.extractPresentableUrl(path);
  }

  protected String getPresentableUrl(@NotNull VirtualFile file) {
    return file.getPresentableName();
  }

  protected abstract T findFileByPathInner(@NotNull String path);

  protected void doRenameFile(VirtualFile vFile, String newName) {
    throw new UnsupportedOperationException("not implemented");
  }

  @Nullable
  public Project getProject(String projectId) {
    List<Project> list = myProject2Id.getKeysByValue(projectId);
    return list == null || list.size() > 1 ? null : list.get(0);
  }

  @NotNull
  public Project getProjectOrFail(String projectId) {
    List<Project> list = myProject2Id.getKeysByValue(projectId);
    if (list == null || list.isEmpty()) throw new AssertionError(projectId + " project not found: " + myProject2Id.values());
    if (list.size() != 1) throw new AssertionError(projectId + " is mapped to several projects: " + list);
    return list.get(0);
  }

  @NotNull
  public Collection<T> getCachedFiles() {
    return myCachedFiles.notNullValues();
  }

  public void onProjectClosed(Project project) {
    myProject2Id.remove(project);
    clearCache();
  }

  public void onProjectOpened(final Project project) {
    // use Disposer instead of ProjectManagerListener#projectClosed() because Disposer.dispose(project)
    // is called later and some cached files should stay valid till the last moment
    Disposer.register(project, new Disposable() {
      @Override
      public void dispose() {
        onProjectClosed(project);
      }
    });

    clearCache();
    String projectId = project.getLocationHash();
    myProject2Id.put(project, projectId);

    List<Project> projects = myProject2Id.getKeysByValue(projectId);
    if (projects != null && projects.size() > 1) {
      LOG.error("project " + projectId + " already registered: " + projects);
    }
  }

  private void initProjectMap() {
    for (Project project : ProjectManager.getInstance().getOpenProjects()) {
      if (project.isOpen()) onProjectOpened(project);
    }
  }

  protected void clearCache() {
    clearInvalidFiles();
  }

  protected void clearInvalidFiles() {
    for (T t : myCachedFiles.notNullValues()) {
      if (!t.isValid()) myCachedFiles.removeValue(t);
    }
    //noinspection StatementWithEmptyBody
    while (myCachedFiles.removeValue(null)) ;
  }

  @TestOnly
  public void cleanup() {
    myCachedFiles.clear();
    myProject2Id.clear();
  }

  @Override
  public void renameFile(Object requestor, @NotNull VirtualFile vFile, @NotNull String newName) throws IOException {
    String oldName = vFile.getName();
    beforeFileRename(vFile, requestor, oldName, newName);
    try {
      doRenameFile(vFile, newName);
    }
    finally {
      fileRenamed(vFile, requestor, oldName, newName);
    }
  }

  protected void beforeFileRename(@NotNull VirtualFile file, Object requestor, @NotNull String oldName, @NotNull String newName) {
    fireBeforePropertyChange(requestor, file, VirtualFile.PROP_NAME, oldName, newName);
    myCachedFiles.remove(file.getPath());
  }

  protected void fileRenamed(@NotNull VirtualFile file, Object requestor, String oldName, String newName) {
    //noinspection unchecked
    myCachedFiles.put(file.getPath(), (T)file);
    firePropertyChanged(requestor, file, VirtualFile.PROP_NAME, oldName, newName);
  }

  protected static String escapeSlash(String name) {
    return name == null ? "" : StringUtil.replace(name, "/", "&slash;");
  }

  protected static String unescapeSlash(String name) {
    return name == null ? "" : StringUtil.replace(name, "&slash;", "/");
  }
}

@@ -1240,24 +1240,75 @@
 FactoryMap;%0A
+import com.intellij.util.containers.ContainerUtil;%0A
 import com.i
@@ -4092,16 +4092,24 @@
 Empty())
+ %7B%0A     
  throw n
@@ -4157,22 +4157,55 @@
 ot found
-: %22 + 
+ among: %22 + ContainerUtil.newArrayList(
 myProjec
@@ -4218,18 +4218,25 @@
 alues())
+)
 ;%0A
+    %7D%0A
     if (
@@ -4252,16 +4252,24 @@
 () != 1)
+ %7B%0A     
  throw n
@@ -4338,24 +4338,30 @@
  %22 + list);%0A
+    %7D%0A
     return l

improve error report 2