/*
 * Copyright 2000-2009 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.ui.treeStructure.filtered;

import com.intellij.ide.util.treeView.AbstractTreeBuilder;
import com.intellij.ide.util.treeView.AbstractTreeStructure;
import com.intellij.ide.util.treeView.NodeDescriptor;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.util.Disposer;
import com.intellij.ui.speedSearch.ElementFilter;
import com.intellij.ui.treeStructure.PatchedDefaultMutableTreeNode;
import com.intellij.ui.treeStructure.SimpleTree;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.ui.tree.TreeUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import java.util.Comparator;

public class FilteringTreeBuilder extends AbstractTreeBuilder {

  private Object myLastSuccessfulSelect;
  private final Tree myTree;

  private MergingUpdateQueue myRefilterQueue;

  public FilteringTreeBuilder(Tree tree,
                              ElementFilter filter,
                              AbstractTreeStructure structure,
                              @Nullable Comparator<NodeDescriptor> comparator) {
    super(tree,
          (DefaultTreeModel)tree.getModel(),
          structure instanceof FilteringTreeStructure ? structure
                                                      : new FilteringTreeStructure(filter, structure),
          comparator);

    myTree = tree;
    initRootNode();

    if (filter instanceof ElementFilter.Active) {
      ((ElementFilter.Active)filter).addListener(new ElementFilter.Listener() {
        @Override
        public ActionCallback update(final Object preferredSelection, final boolean adjustSelection, final boolean now) {
          return refilter(preferredSelection, adjustSelection, now);
        }
      }, this);
    }

    myTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
      @Override
      public void valueChanged(TreeSelectionEvent e) {
        TreePath newPath = e.getNewLeadSelectionPath();
        if (newPath != null) {
          Object element = getElementFor(newPath.getLastPathComponent());
          if (element != null) {
            myLastSuccessfulSelect = element;
          }
        }
      }
    });
  }

  @Override
  public boolean isAlwaysShowPlus(NodeDescriptor nodeDescriptor) {
    return false;
  }

  @Override
  public boolean isAutoExpandNode(NodeDescriptor nodeDescriptor) {
    return true;
  }

  protected final DefaultMutableTreeNode createChildNode(final NodeDescriptor childDescr) {
    return new PatchedDefaultMutableTreeNode(childDescr);
  }

  public void setFilteringMerge(int gap, @Nullable JComponent modalityStateComponent) {
    if (myRefilterQueue != null) {
      Disposer.dispose(myRefilterQueue);
      myRefilterQueue = null;
    }

    if (gap >= 0) {
      JComponent stateComponent = modalityStateComponent;
      if (stateComponent == null) {
        stateComponent = myTree;
      }

      myRefilterQueue = new MergingUpdateQueue("FilteringTreeBuilder", gap, false, stateComponent, this, myTree);
      myRefilterQueue.setRestartTimerOnAdd(true);
    }
  }

  protected boolean isSelectable(Object nodeObject) {
    return true;
  }

  public ActionCallback refilter() {
    return refilter(null, true, false);
  }

  public ActionCallback refilter(@Nullable final Object preferredSelection, final boolean adjustSelection, final boolean now) {
    if (myRefilterQueue != null) {
      myRefilterQueue.cancelAllUpdates();
    }
    final ActionCallback callback = new ActionCallback();
    final Runnable afterCancelUpdate = new Runnable() {
      @Override
      public void run() {
        if (myRefilterQueue == null || now) {
          refilterNow(preferredSelection, adjustSelection).doWhenDone(callback.createSetDoneRunnable());
        }
        else {
          myRefilterQueue.queue(new Update(this) {
            @Override
            public void run() {
              refilterNow(preferredSelection, adjustSelection).notifyWhenDone(callback);
            }

            @Override
            public void setRejected() {
              super.setRejected();
              callback.setDone();
            }
          });
        }
      }
    };
    if (!isDisposed()) {
      if (!ApplicationManager.getApplication().isUnitTestMode()) {
        getUi().cancelUpdate().doWhenProcessed(afterCancelUpdate);
      } else {
        afterCancelUpdate.run();
      }
    }

    return callback;
  }


  protected ActionCallback refilterNow(final Object preferredSelection, final boolean adjustSelection) {
    final ActionCallback selectionDone = new ActionCallback();

    getFilteredStructure().refilter();
    getUi().updateSubtree(getRootNode(), false);
    final Runnable selectionRunnable = () -> {
      revalidateTree();

      Object toSelect = preferredSelection != null ? preferredSelection : myLastSuccessfulSelect;

      if (adjustSelection && toSelect != null) {
        final FilteringTreeStructure.FilteringNode nodeToSelect = getFilteredStructure().getVisibleNodeFor(toSelect);

        if (nodeToSelect != null) {
          select(nodeToSelect, () -> {
            if (getSelectedElements().contains(nodeToSelect)) {
              myLastSuccessfulSelect = getOriginalNode(nodeToSelect);
            }
            selectionDone.setDone();
          });
        }
        else {
          TreeUtil.ensureSelection(myTree);
          selectionDone.setDone();
        }
      }
      else {
        selectionDone.setDone();
      }
    };
    if (!ApplicationManager.getApplication().isUnitTestMode()) {
      queueUpdate().doWhenProcessed(selectionRunnable);
    } else {
      selectionRunnable.run();
    }

    final ActionCallback result = new ActionCallback();

    selectionDone.doWhenDone(new Runnable() {
      @Override
      public void run() {
        if (!ApplicationManager.getApplication().isUnitTestMode()) {
          scrollSelectionToVisible(new Runnable() {
            @Override
            public void run() {
              getReady(this).notify(result);
            }
          }, false);
        } else {
          result.setDone();
        }
      }
    }).notifyWhenRejected(result);

    return result;
  }

  public void revalidateTree() {
    revalidateTree(myTree);
  }

  public static void revalidateTree(Tree tree) {
    tree.invalidate();
    tree.setRowHeight(tree.getRowHeight() == -1 ? -2 : -1);
    tree.revalidate();
    tree.repaint();
  }


  private FilteringTreeStructure getFilteredStructure() {
    return ((FilteringTreeStructure)getTreeStructure());
  }

  //todo kirillk
  private boolean isSimpleTree() {
    return myTree instanceof SimpleTree;
  }

  @Nullable
  private Object getSelected() {
    if (isSimpleTree()) {
      FilteringTreeStructure.FilteringNode selected = (FilteringTreeStructure.FilteringNode)((SimpleTree)myTree).getSelectedNode();
      return selected != null ? selected.getDelegate() : null;
    } else {
      final Object[] nodes = myTree.getSelectedNodes(Object.class, null);
      return nodes.length > 0 ? nodes[0] : null;
    }
  }

  public FilteringTreeStructure.FilteringNode getVisibleNodeFor(Object nodeObject) {
    FilteringTreeStructure structure = getFilteredStructure();
    return structure != null ? structure.getVisibleNodeFor(nodeObject) : null;
  }

  public Object getOriginalNode(Object node) {
    return ((FilteringTreeStructure.FilteringNode)node).getDelegate();
  }

  @Override
  protected Object transformElement(Object object) {
    return getOriginalNode(object);
  }

  @Nullable
  public Object getElementFor(Object node) {
    return getUi().getElementFor(node);
  }
}
