/*
 * Copyright 2000-2011 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.ide.favoritesTreeView;

import com.intellij.ide.IdeBundle;
import com.intellij.ide.favoritesTreeView.actions.AddToFavoritesAction;
import com.intellij.ide.projectView.impl.*;
import com.intellij.ide.projectView.impl.nodes.LibraryGroupElement;
import com.intellij.ide.projectView.impl.nodes.NamedLibraryElement;
import com.intellij.ide.util.treeView.AbstractTreeNode;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.project.DumbAwareRunnable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ContentIterator;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.ui.InputValidator;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.TreeItem;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
import consulo.disposer.Disposable;
import consulo.disposer.Disposer;
import consulo.roots.types.BinariesOrderRootType;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import javax.annotation.Nonnull;

import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;

import static com.intellij.ide.favoritesTreeView.FavoritesListProvider.EP_NAME;

@Singleton
public class FavoritesManager implements ProjectComponent, JDOMExternalizable {
  private final ArrayList<String> myListOrder = new ArrayList<String>();
  // fav list name -> list of (root: root url, root class)
  private final Map<String, List<TreeItem<Pair<AbstractUrl, String>>>> myName2FavoritesRoots =
          new TreeMap<String, List<TreeItem<Pair<AbstractUrl, String>>>>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
              return myListOrder.indexOf(o1) - myListOrder.indexOf(o2);
            }
          });
  private final Map<String, String> myDescriptions = new HashMap<String, String>();
  private final Project myProject;
  private final List<FavoritesListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
  private final FavoritesViewSettings myViewSettings = new FavoritesViewSettings();
  private final Map<String, FavoritesListProvider> myProviders = new HashMap<String, FavoritesListProvider>();

  private void rootsChanged() {
    for (FavoritesListener listener : myListeners) {
      listener.rootsChanged();
    }
  }

  private void listAdded(String listName) {
    for (FavoritesListener listener : myListeners) {
      listener.listAdded(listName);
    }
  }

  private void listRemoved(String listName) {
    for (FavoritesListener listener : myListeners) {
      listener.listRemoved(listName);
    }
  }

  public void renameList(final Project project, @Nonnull String listName) {
    final String newName = Messages.showInputDialog(project, IdeBundle.message("prompt.input.favorites.list.new.name", listName),
                                                    IdeBundle.message("title.rename.favorites.list"), Messages.getInformationIcon(), listName,
                                                    new InputValidator() {
                                                      @Override
                                                      public boolean checkInput(String inputString) {
                                                        return inputString != null && inputString.trim().length() > 0;
                                                      }

                                                      @Override
                                                      public boolean canClose(String inputString) {
                                                        inputString = inputString.trim();
                                                        if (myName2FavoritesRoots.keySet().contains(inputString) ||
                                                            myProviders.keySet().contains(inputString)) {
                                                          Messages.showErrorDialog(project,
                                                                                   IdeBundle.message("error.favorites.list.already.exists", inputString.trim()),
                                                                                   IdeBundle.message("title.unable.to.add.favorites.list"));
                                                          return false;
                                                        }
                                                        return !inputString.isEmpty();
                                                      }
                                                    });

    if (newName != null && renameFavoritesList(listName, newName)) {
      rootsChanged();
    }
  }

  @Deprecated
  public void addFavoritesListener(FavoritesListener listener) {
    myListeners.add(listener);
    listener.rootsChanged();
  }

  public void addFavoritesListener(final FavoritesListener listener, @Nonnull Disposable parent) {
    myListeners.add(listener);
    listener.rootsChanged();
    Disposer.register(parent, new Disposable() {
      @Override
      public void dispose() {
        myListeners.remove(listener);
      }
    });
  }

  public void removeFavoritesListener(FavoritesListener listener) {
    myListeners.remove(listener);
  }

  List<AbstractTreeNode> createRootNodes() {
    List<AbstractTreeNode> result = new ArrayList<AbstractTreeNode>();
    for (String listName : myName2FavoritesRoots.keySet()) {
      result.add(new FavoritesListNode(myProject, listName, myDescriptions.get(listName)));
    }
    ArrayList<FavoritesListProvider> providers = new ArrayList<FavoritesListProvider>(myProviders.values());
    Collections.sort(providers);
    for (FavoritesListProvider provider : providers) {
      result.add(provider.createFavoriteListNode(myProject));
    }
    return result;
  }

  public static FavoritesManager getInstance(Project project) {
    return project.getComponent(FavoritesManager.class);
  }

  @Inject
  public FavoritesManager(Project project) {
    myProject = project;
  }

  @Nonnull
  public List<String> getAvailableFavoritesListNames() {
    return new ArrayList<String>(myName2FavoritesRoots.keySet());
  }

  public synchronized void createNewList(@Nonnull String listName) {
    myListOrder.add(listName);
    myName2FavoritesRoots.put(listName, new ArrayList<TreeItem<Pair<AbstractUrl, String>>>());
    listAdded(listName);
  }

  public synchronized void fireListeners(@Nonnull final String listName) {
    rootsChanged();
  }

  public FavoritesViewSettings getViewSettings() {
    return myViewSettings;
  }

  public synchronized boolean removeFavoritesList(@Nonnull String name) {
    boolean result = myName2FavoritesRoots.remove(name) != null;
    myListOrder.remove(name);
    myDescriptions.remove(name);
    listRemoved(name);
    return result;
  }

  @Nonnull
  public List<TreeItem<Pair<AbstractUrl, String>>> getFavoritesListRootUrls(@Nonnull String name) {
    final List<TreeItem<Pair<AbstractUrl, String>>> pairs = myName2FavoritesRoots.get(name);
    return pairs == null ? new ArrayList<TreeItem<Pair<AbstractUrl, String>>>() : pairs;
  }

  public synchronized boolean addRoots(@Nonnull String name, Module moduleContext, @Nonnull Object elements) {
    Collection<AbstractTreeNode> nodes = AddToFavoritesAction.createNodes(myProject, moduleContext, elements, true, getViewSettings());
    return !nodes.isEmpty() && addRoots(name, nodes);
  }

  public synchronized Comparator<FavoritesTreeNodeDescriptor> getCustomComparator(@Nonnull final String name) {
    return myProviders.get(name);
  }

  private Pair<AbstractUrl, String> createPairForNode(AbstractTreeNode node) {
    final String className = node.getClass().getName();
    final Object value = node.getValue();
    final AbstractUrl url = createUrlByElement(value, myProject);
    if (url == null) return null;
    return Pair.create(url, className);
  }

  public boolean addRoots(final String name, final Collection<AbstractTreeNode> nodes) {
    final Collection<TreeItem<Pair<AbstractUrl, String>>> list = getFavoritesListRootUrls(name);

    final HashSet<AbstractUrl> set = new HashSet<AbstractUrl>(ContainerUtil.map(list, new Function<TreeItem<Pair<AbstractUrl, String>>, AbstractUrl>() {
      @Override
      public AbstractUrl fun(TreeItem<Pair<AbstractUrl, String>> item) {
        return item.getData().getFirst();
      }
    }));
    for (AbstractTreeNode node : nodes) {
      final Pair<AbstractUrl, String> pair = createPairForNode(node);
      if (pair != null) {
        if (set.contains(pair.getFirst())) continue;
        final TreeItem<Pair<AbstractUrl, String>> treeItem = new TreeItem<Pair<AbstractUrl, String>>(pair);
        list.add(treeItem);
        set.add(pair.getFirst());
        appendChildNodes(node, treeItem);
      }
    }
    rootsChanged();
    return true;
  }

  private void appendChildNodes(AbstractTreeNode node, TreeItem<Pair<AbstractUrl, String>> treeItem) {
    final Collection<? extends AbstractTreeNode> children = node.getChildren();
    for (AbstractTreeNode child : children) {
      final TreeItem<Pair<AbstractUrl, String>> childTreeItem = new TreeItem<Pair<AbstractUrl, String>>(createPairForNode(child));
      treeItem.addChild(childTreeItem);
      appendChildNodes(child, childTreeItem);
    }
  }

  public synchronized boolean addRoot(@Nonnull String name,
                                      @Nonnull List<AbstractTreeNode> parentElements,
                                      final AbstractTreeNode newElement,
                                      @Nullable AbstractTreeNode sibling) {
    final List<TreeItem<Pair<AbstractUrl, String>>> items = myName2FavoritesRoots.get(name);
    if (items == null) return false;
    AbstractUrl url = createUrlByElement(newElement.getValue(), myProject);
    if (url == null) return false;
    final TreeItem<Pair<AbstractUrl, String>> newItem = new TreeItem<Pair<AbstractUrl, String>>(Pair.create(url, newElement.getClass().getName()));

    if (parentElements.isEmpty()) {
      // directly to list
      if (sibling != null) {
        TreeItem<Pair<AbstractUrl, String>> after = null;
        AbstractUrl siblingUrl = createUrlByElement(sibling.getValue(), myProject);
        int idx = -1;
        for (int i = 0; i < items.size(); i++) {
          TreeItem<Pair<AbstractUrl, String>> item = items.get(i);
          if (item.getData().getFirst().equals(siblingUrl)) {
            idx = i;
            break;
          }
        }
        if (idx != -1) {
          items.add(idx, newItem);
        }
        else {
          items.add(newItem);
        }
      }
      else {
        items.add(newItem);
      }

      rootsChanged();
      return true;
    }

    Collection<TreeItem<Pair<AbstractUrl, String>>> list = items;
    TreeItem<Pair<AbstractUrl, String>> item = null;
    for (AbstractTreeNode obj : parentElements) {
      AbstractUrl objUrl = createUrlByElement(obj.getValue(), myProject);
      item = findNextItem(objUrl, list);
      if (item == null) return false;
      list = item.getChildren();
    }

    if (sibling != null) {
      TreeItem<Pair<AbstractUrl, String>> after = null;
      AbstractUrl siblingUrl = createUrlByElement(sibling.getValue(), myProject);
      for (TreeItem<Pair<AbstractUrl, String>> treeItem : list) {
        if (treeItem.getData().getFirst().equals(siblingUrl)) {
          after = treeItem;
          break;
        }
      }
      if (after == null) {
        item.addChild(newItem);
      }
      else {
        item.addChildAfter(newItem, after);
      }
    }
    else {
      item.addChild(newItem);
    }
    rootsChanged();
    return true;
  }

  private <T> boolean findListToRemoveFrom(@Nonnull String name, @Nonnull final List<T> elements, final Convertor<T, AbstractUrl> convertor) {
    Collection<TreeItem<Pair<AbstractUrl, String>>> list = getFavoritesListRootUrls(name);
    if (elements.size() > 1) {
      final List<T> sublist = elements.subList(0, elements.size() - 1);
      for (T obj : sublist) {
        AbstractUrl objUrl = convertor.convert(obj);
        final TreeItem<Pair<AbstractUrl, String>> item = findNextItem(objUrl, list);
        if (item == null || item.getChildren() == null) return false;
        list = item.getChildren();
      }
    }

    TreeItem<Pair<AbstractUrl, String>> found = null;
    AbstractUrl url = convertor.convert(elements.get(elements.size() - 1));
    if (url == null) return false;
    for (TreeItem<Pair<AbstractUrl, String>> pair : list) {
      if (url.equals(pair.getData().getFirst())) {
        found = pair;
        break;
      }
    }

    if (found != null) {
      list.remove(found);
      rootsChanged();
      return true;
    }
    return false;
  }

  public synchronized boolean removeRoot(@Nonnull String name, @Nonnull List<AbstractTreeNode> elements) {
    final Convertor<AbstractTreeNode, AbstractUrl> convertor = new Convertor<AbstractTreeNode, AbstractUrl>() {
      @Override
      public AbstractUrl convert(AbstractTreeNode obj) {
        return createUrlByElement(obj.getValue(), myProject);
      }
    };
    boolean result = true;
    for (AbstractTreeNode element : elements) {
      final List<AbstractTreeNode> path = TaskDefaultFavoriteListProvider.getPathToUsualNode(element);
      result &= findListToRemoveFrom(name, path.subList(1, path.size()), convertor);
    }
    return result;
  }

  private TreeItem<Pair<AbstractUrl, String>> findNextItem(AbstractUrl url, Collection<TreeItem<Pair<AbstractUrl, String>>> list) {
    for (TreeItem<Pair<AbstractUrl, String>> pair : list) {
      if (url.equals(pair.getData().getFirst())) {
        return pair;
      }
    }
    return null;
  }

  private boolean renameFavoritesList(@Nonnull String oldName, @Nonnull String newName) {
    List<TreeItem<Pair<AbstractUrl, String>>> list = myName2FavoritesRoots.remove(oldName);
    if (list != null && newName.length() > 0) {
      int index = myListOrder.indexOf(oldName);
      if (index == -1) {
        index = myListOrder.size();
      }
      myListOrder.set(index, newName);
      myName2FavoritesRoots.put(newName, list);
      String description = myDescriptions.remove(oldName);
      if (description != null) {
        myDescriptions.put(newName, description);
      }
      rootsChanged();
      return true;
    }
    return false;
  }

  @Override
  public void projectOpened() {
    if (!ApplicationManager.getApplication().isUnitTestMode()) {
      StartupManager.getInstance(myProject).registerPostStartupActivity(new DumbAwareRunnable() {
        @Override
        public void run() {
          final FavoritesListProvider[] providers = Extensions.getExtensions(EP_NAME, myProject);
          for (FavoritesListProvider provider : providers) {
            myProviders.put(provider.getListName(myProject), provider);
          }
          final MyRootsChangeAdapter myPsiTreeChangeAdapter = new MyRootsChangeAdapter();

          PsiManager.getInstance(myProject).addPsiTreeChangeListener(myPsiTreeChangeAdapter, myProject);
          if (myName2FavoritesRoots.isEmpty()) {
            myDescriptions.put(myProject.getName(), "auto-added");
            createNewList(myProject.getName());
          }
        }
      });
    }
  }

  @Override
  @Nonnull
  public String getComponentName() {
    return "FavoritesManager";
  }

  @Nullable
  public FavoritesListProvider getListProvider(@Nullable String name) {
    return myProviders.get(name);
  }

  @Override
  public void readExternal(Element element) throws InvalidDataException {
    myName2FavoritesRoots.clear();
    for (Object list : element.getChildren(ELEMENT_FAVORITES_LIST)) {
      final String name = ((Element)list).getAttributeValue(ATTRIBUTE_NAME);
      List<TreeItem<Pair<AbstractUrl, String>>> roots = readRoots((Element)list, myProject);
      myListOrder.add(name);
      myName2FavoritesRoots.put(name, roots);
    }
    DefaultJDOMExternalizer.readExternal(this, element);
  }

  @NonNls private static final String CLASS_NAME = "klass";
  @NonNls private static final String FAVORITES_ROOT = "favorite_root";
  @NonNls private static final String ELEMENT_FAVORITES_LIST = "favorites_list";
  @NonNls private static final String ATTRIBUTE_NAME = "name";

  private static List<TreeItem<Pair<AbstractUrl, String>>> readRoots(final Element list, Project project) {
    List<TreeItem<Pair<AbstractUrl, String>>> result = new ArrayList<TreeItem<Pair<AbstractUrl, String>>>();
    readFavoritesOneLevel(list, project, result);
    return result;
  }

  private static void readFavoritesOneLevel(Element list, Project project, Collection<TreeItem<Pair<AbstractUrl, String>>> result) {
    final List listChildren = list.getChildren(FAVORITES_ROOT);
    if (listChildren == null || listChildren.isEmpty()) return;

    for (Object favorite : listChildren) {
      final Element favoriteElement = (Element)favorite;
      final String className = favoriteElement.getAttributeValue(CLASS_NAME);
      final AbstractUrl abstractUrl = readUrlFromElement(favoriteElement, project);
      if (abstractUrl != null) {
        final TreeItem<Pair<AbstractUrl, String>> treeItem = new TreeItem<Pair<AbstractUrl, String>>(Pair.create(abstractUrl, className));
        result.add(treeItem);
        readFavoritesOneLevel(favoriteElement, project, treeItem.getChildren());
      }
    }
  }

  private static final ArrayList<AbstractUrl> ourAbstractUrlProviders = new ArrayList<AbstractUrl>();

  static {
    ourAbstractUrlProviders.add(new ModuleUrl(null, null));
    ourAbstractUrlProviders.add(new DirectoryUrl(null, null));

    ourAbstractUrlProviders.add(new ModuleGroupUrl(null));

    ourAbstractUrlProviders.add(new PsiFileUrl(null));
    ourAbstractUrlProviders.add(new LibraryModuleGroupUrl(null));
    ourAbstractUrlProviders.add(new NamedLibraryUrl(null, null));
  }

  @NonNls private static final String ATTRIBUTE_TYPE = "type";
  @NonNls private static final String ATTRIBUTE_URL = "url";
  @NonNls private static final String ATTRIBUTE_MODULE = "module";

  @Nullable
  private static AbstractUrl readUrlFromElement(Element element, Project project) {
    final String type = element.getAttributeValue(ATTRIBUTE_TYPE);
    final String urlValue = element.getAttributeValue(ATTRIBUTE_URL);
    final String moduleName = element.getAttributeValue(ATTRIBUTE_MODULE);

    for (FavoriteNodeProvider nodeProvider : Extensions.getExtensions(FavoriteNodeProvider.EP_NAME, project)) {
      if (nodeProvider.getFavoriteTypeId().equals(type)) {
        return new AbstractUrlFavoriteAdapter(urlValue, moduleName, nodeProvider);
      }
    }

    for (AbstractUrl urlProvider : ourAbstractUrlProviders) {
      AbstractUrl url = urlProvider.createUrl(type, moduleName, urlValue);
      if (url != null) return url;
    }
    return null;
  }


  @Override
  public void writeExternal(Element element) throws WriteExternalException {
    for (final String name : myName2FavoritesRoots.keySet()) {
      Element list = new Element(ELEMENT_FAVORITES_LIST);
      list.setAttribute(ATTRIBUTE_NAME, name);
      writeRoots(list, myName2FavoritesRoots.get(name));
      element.addContent(list);
    }
    DefaultJDOMExternalizer.writeExternal(this, element);
  }

  @Nullable
  public static AbstractUrl createUrlByElement(Object element, final Project project) {
    if (element instanceof SmartPsiElementPointer) element = ((SmartPsiElementPointer)element).getElement();

    for (FavoriteNodeProvider nodeProvider : Extensions.getExtensions(FavoriteNodeProvider.EP_NAME, project)) {
      String url = nodeProvider.getElementUrl(element);
      if (url != null) {
        return new AbstractUrlFavoriteAdapter(url, nodeProvider.getElementModuleName(element), nodeProvider);
      }
    }

    for (AbstractUrl urlProvider : ourAbstractUrlProviders) {
      AbstractUrl url = urlProvider.createUrlByElement(element);
      if (url != null) return url;
    }
    return null;
  }

  private static void writeRoots(Element element, Collection<TreeItem<Pair<AbstractUrl, String>>> roots) {
    for (TreeItem<Pair<AbstractUrl, String>> root : roots) {
      final AbstractUrl url = root.getData().getFirst();
      if (url == null) continue;
      final Element list = new Element(FAVORITES_ROOT);
      url.write(list);
      list.setAttribute(CLASS_NAME, root.getData().getSecond());
      element.addContent(list);
      final List<TreeItem<Pair<AbstractUrl, String>>> children = root.getChildren();
      if (children != null && !children.isEmpty()) {
        writeRoots(list, children);
      }
    }
  }

  public String getFavoriteListName(@Nullable final String currentSubId, @Nonnull final VirtualFile vFile) {
    if (currentSubId != null && contains(currentSubId, vFile)) {
      return currentSubId;
    }
    for (String listName : myName2FavoritesRoots.keySet()) {
      if (contains(listName, vFile)) {
        return listName;
      }
    }
    return null;
  }

  // currently only one level here..
  public boolean contains(@Nonnull String name, @Nonnull final VirtualFile vFile) {
    final ProjectFileIndex projectFileIndex = ProjectRootManager.getInstance(myProject).getFileIndex();
    final Set<Boolean> find = new HashSet<Boolean>();
    final ContentIterator contentIterator = new ContentIterator() {
      @Override
      public boolean processFile(VirtualFile fileOrDir) {
        if (fileOrDir != null && fileOrDir.getPath().equals(vFile.getPath())) {
          find.add(Boolean.TRUE);
        }
        return true;
      }
    };

    Collection<TreeItem<Pair<AbstractUrl, String>>> urls = getFavoritesListRootUrls(name);
    for (TreeItem<Pair<AbstractUrl, String>> pair : urls) {
      AbstractUrl abstractUrl = pair.getData().getFirst();
      if (abstractUrl == null) {
        continue;
      }
      final Object[] path = abstractUrl.createPath(myProject);
      if (path == null || path.length < 1 || path[0] == null) {
        continue;
      }
      Object element = path[path.length - 1];
      if (element instanceof SmartPsiElementPointer) {
        final VirtualFile virtualFile = PsiUtilBase.getVirtualFile(((SmartPsiElementPointer)element).getElement());
        if (virtualFile == null) continue;
        if (vFile.getPath().equals(virtualFile.getPath())) {
          return true;
        }
        if (!virtualFile.isDirectory()) {
          continue;
        }
        projectFileIndex.iterateContentUnderDirectory(virtualFile, contentIterator);
      }

      if (element instanceof PsiElement) {
        final VirtualFile virtualFile = PsiUtilBase.getVirtualFile((PsiElement)element);
        if (virtualFile == null) continue;
        if (vFile.getPath().equals(virtualFile.getPath())) {
          return true;
        }
        if (!virtualFile.isDirectory()) {
          continue;
        }
        projectFileIndex.iterateContentUnderDirectory(virtualFile, contentIterator);
      }
      if (element instanceof Module) {
        ModuleRootManager.getInstance((Module)element).getFileIndex().iterateContent(contentIterator);
      }
      if (element instanceof LibraryGroupElement) {
        final boolean inLibrary = ModuleRootManager.getInstance(((LibraryGroupElement)element).getModule()).getFileIndex().isInContent(vFile) &&
                                  projectFileIndex.isInLibraryClasses(vFile);
        if (inLibrary) {
          return true;
        }
      }
      if (element instanceof NamedLibraryElement) {
        NamedLibraryElement namedLibraryElement = (NamedLibraryElement)element;
        final VirtualFile[] files = namedLibraryElement.getOrderEntry().getFiles(BinariesOrderRootType.getInstance());
        if (files != null && ArrayUtil.find(files, vFile) > -1) {
          return true;
        }
      }
      if (element instanceof ModuleGroup) {
        ModuleGroup group = (ModuleGroup)element;
        final Collection<Module> modules = group.modulesInGroup(myProject, true);
        for (Module module : modules) {
          ModuleRootManager.getInstance(module).getFileIndex().iterateContent(contentIterator);
        }
      }


      for (FavoriteNodeProvider provider : Extensions.getExtensions(FavoriteNodeProvider.EP_NAME, myProject)) {
        if (provider.elementContainsFile(element, vFile)) {
          return true;
        }
      }

      if (!find.isEmpty()) {
        return true;
      }
    }
    return false;
  }

  private static void iterateTreeItems(final Collection<TreeItem<Pair<AbstractUrl, String>>> coll, Consumer<TreeItem<Pair<AbstractUrl, String>>> consumer) {
    final ArrayDeque<TreeItem<Pair<AbstractUrl, String>>> queue = new ArrayDeque<TreeItem<Pair<AbstractUrl, String>>>();
    queue.addAll(coll);
    while (!queue.isEmpty()) {
      final TreeItem<Pair<AbstractUrl, String>> item = queue.removeFirst();
      consumer.consume(item);
      final List<TreeItem<Pair<AbstractUrl, String>>> children = item.getChildren();
      if (children != null && !children.isEmpty()) {
        queue.addAll(children);
      }
    }
  }

  private class MyRootsChangeAdapter extends PsiTreeChangeAdapter {
    @Override
    public void beforeChildMovement(@Nonnull final PsiTreeChangeEvent event) {
      final PsiElement oldParent = event.getOldParent();
      final PsiElement newParent = event.getNewParent();
      final PsiElement child = event.getChild();
      if (newParent instanceof PsiDirectory) {
        final Module module = ModuleUtil.findModuleForPsiElement(newParent);
        if (module == null) return;
        AbstractUrl childUrl = null;
        if (child instanceof PsiFile) {
          childUrl = new PsiFileUrl(((PsiDirectory)newParent).getVirtualFile().getUrl() + "/" + ((PsiFile)child).getName());
        }
        else if (child instanceof PsiDirectory) {
          childUrl = new DirectoryUrl(((PsiDirectory)newParent).getVirtualFile().getUrl() + "/" + ((PsiDirectory)child).getName(), module.getName());
        }

        for (String listName : myName2FavoritesRoots.keySet()) {
          final List<TreeItem<Pair<AbstractUrl, String>>> roots = myName2FavoritesRoots.get(listName);
          final AbstractUrl finalChildUrl = childUrl;
          iterateTreeItems(roots, new Consumer<TreeItem<Pair<AbstractUrl, String>>>() {
            @Override
            public void consume(TreeItem<Pair<AbstractUrl, String>> item) {
              final Pair<AbstractUrl, String> root = item.getData();
              final Object[] path = root.first.createPath(myProject);
              if (path == null || path.length < 1 || path[0] == null) {
                return;
              }
              final Object element = path[path.length - 1];
              if (element == child && finalChildUrl != null) {
                item.setData(Pair.create(finalChildUrl, root.second));
              }
              else {
                if (element == oldParent) {
                  item.setData(Pair.create(root.first.createUrlByElement(newParent), root.second));
                }
              }
            }
          });
        }
      }
    }

    @Override
    public void beforePropertyChange(@Nonnull final PsiTreeChangeEvent event) {
      if (event.getPropertyName().equals(PsiTreeChangeEvent.PROP_FILE_NAME) || event.getPropertyName().equals(PsiTreeChangeEvent.PROP_DIRECTORY_NAME)) {
        final PsiElement psiElement = event.getChild();
        if (psiElement instanceof PsiFile || psiElement instanceof PsiDirectory) {
          final Module module = ModuleUtil.findModuleForPsiElement(psiElement);
          if (module == null) return;
          final String url = ((PsiDirectory)psiElement.getParent()).getVirtualFile().getUrl() + "/" + event.getNewValue();
          final AbstractUrl childUrl = psiElement instanceof PsiFile ? new PsiFileUrl(url) : new DirectoryUrl(url, module.getName());

          for (String listName : myName2FavoritesRoots.keySet()) {
            final List<TreeItem<Pair<AbstractUrl, String>>> roots = myName2FavoritesRoots.get(listName);
            iterateTreeItems(roots, new Consumer<TreeItem<Pair<AbstractUrl, String>>>() {
              @Override
              public void consume(TreeItem<Pair<AbstractUrl, String>> item) {
                final Pair<AbstractUrl, String> root = item.getData();
                final Object[] path = root.first.createPath(myProject);
                if (path == null || path.length < 1 || path[0] == null) {
                  return;
                }
                final Object element = path[path.length - 1];
                if (element == psiElement && psiElement instanceof PsiFile) {
                  item.setData(Pair.create(childUrl, root.second));
                }
                else {
                  item.setData(root);
                }
              }
            });
          }
        }
      }
    }
  }
}