/*
 * Copyright 2000-2016 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.keymap.impl;

import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.ide.DataManager;
import com.intellij.ide.IdeEventQueue;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
import com.intellij.openapi.actionSystem.ex.ActionUtil;
import com.intellij.openapi.actionSystem.impl.PresentationFactory;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.keymap.impl.ui.MouseShortcutPanel;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.openapi.wm.impl.FocusManagerImpl;
import com.intellij.openapi.wm.impl.IdeGlassPaneImpl;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.ui.UIUtil;
import org.intellij.lang.annotations.JdkConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static com.intellij.Patches.JDK_BUG_ID_8147994;
import static java.awt.event.MouseEvent.*;

/**
 * Current implementation of the dispatcher is intended to filter mouse event addressed to
 * the editor. Also it allows to map middle mouse's button to some action.
 *
 * @author Vladimir Kondratyev
 * @author Konstantin Bulenkov
 */
public final class IdeMouseEventDispatcher {
  private final PresentationFactory myPresentationFactory = new PresentationFactory();
  private final ArrayList<AnAction> myActions = new ArrayList<>(1);
  private final Map<Container, BlockState> myRootPane2BlockedId = new HashMap<>();
  private int myLastHorScrolledComponentHash;
  private boolean myPressedModifiersStored;
  @JdkConstants.InputEventMask
  private int myModifiers;
  @JdkConstants.InputEventMask
  private int myModifiersEx;

  private static boolean myForceTouchIsAllowed = true;

  public static void forbidForceTouch () {
    myForceTouchIsAllowed = false;
  }

  public static boolean isForceTouchAllowed () {
    return myForceTouchIsAllowed;
  }

  // Don't compare MouseEvent ids. Swing has wrong sequence of events: first is mouse_clicked(500)
  // then mouse_pressed(501), mouse_released(502) etc. Here, mouse events sorted so we can compare
  // theirs ids to properly use method blockNextEvents(MouseEvent)
  private static final List<Integer> SWING_EVENTS_PRIORITY = Arrays.asList(MOUSE_PRESSED,
                                                                           MOUSE_ENTERED,
                                                                           MOUSE_EXITED,
                                                                           MOUSE_MOVED,
                                                                           MOUSE_DRAGGED,
                                                                           MOUSE_WHEEL,
                                                                           MOUSE_RELEASED,
                                                                           MOUSE_CLICKED);

  public IdeMouseEventDispatcher() {
  }

  private void fillActionsList(Component component, MouseShortcut mouseShortcut, boolean isModalContext) {
    myActions.clear();

    // here we try to find "local" shortcuts
    for (; component != null; component = component.getParent()) {
      if (component instanceof JComponent) {
        for (AnAction action : ActionUtil.getActions((JComponent)component)) {
          for (Shortcut shortcut : action.getShortcutSet().getShortcuts()) {
            if (mouseShortcut.equals(shortcut) && !myActions.contains(action)) {
              myActions.add(action);
            }
          }
        }
        // once we've found a proper local shortcut(s), we exit
        if (!myActions.isEmpty()) {
          return;
        }
      }
    }

    // search in main keymap
    KeymapManager keymapManager = KeymapManagerImpl.ourKeymapManagerInitialized ? KeymapManager.getInstance() : null;
    if (keymapManager != null) {
      Keymap keymap = keymapManager.getActiveKeymap();
      ActionManager actionManager = ActionManager.getInstance();
      for (String actionId : keymap.getActionIds(mouseShortcut)) {
        AnAction action = actionManager.getAction(actionId);
        if (action == null || isModalContext && !action.isEnabledInModalContext()) {
          continue;
        }

        if (!myActions.contains(action)) {
          myActions.add(action);
        }
      }
    }
  }

  /**
   * @return {@code true} if and only if the passed event is already dispatched by the
   *         {@code IdeMouseEventDispatcher} and there is no need for any other processing of the event.
   *         If the method returns {@code false} then it means that the event should be delivered
   *         to normal event dispatching.
   */
  public boolean dispatchMouseEvent(MouseEvent e) {
    Component c = e.getComponent();

    //frame activation by mouse click
    if (e.getID() == MOUSE_PRESSED && c instanceof IdeFrame && !c.hasFocus()) {
      IdeFocusManager focusManager = IdeFocusManager.getGlobalInstance();
      if (focusManager instanceof FocusManagerImpl) {
        Component at = SwingUtilities.getDeepestComponentAt(c, e.getX(), e.getY());
        if (at != null && at.isFocusable()) {
          ((FocusManagerImpl)focusManager).setLastFocusedAtDeactivation((IdeFrame)c, at);
        }
      }
    }

    if (e.isPopupTrigger()) {
      if (BUTTON3 == e.getButton()) {
        if (Registry.is("ide.mouse.popup.trigger.modifiers.disabled") && (~BUTTON3_DOWN_MASK & e.getModifiersEx()) != 0) {
          // it allows to use our mouse shortcuts for Ctrl+Button3, for example
          resetPopupTrigger(e);
        }
      }
      else if (SystemInfo.isXWindow) {
        // we can do better than silly triggering popup on everything but left click
        resetPopupTrigger(e);
      }
    }

    boolean ignore = false;
    if (!(e.getID() == MOUSE_PRESSED ||
          e.getID() == MOUSE_RELEASED ||
          e.getID() == MOUSE_WHEEL && 0 < e.getModifiersEx() ||
          e.getID() == MOUSE_CLICKED)) {
      ignore = true;
    }

    patchClickCount(e);

    int clickCount = e.getClickCount();
    int button = MouseShortcut.getButton(e);
    if (button == MouseShortcut.BUTTON_WHEEL_UP || button == MouseShortcut.BUTTON_WHEEL_DOWN) {
      clickCount = 1;
    }

    if (e.isConsumed()
        || e.isPopupTrigger()
        || (button > 3 ? e.getID() != MOUSE_PRESSED && e.getID() != MOUSE_WHEEL : e.getID() != MOUSE_RELEASED)
        || clickCount < 1
        || button == NOBUTTON) { // See #16995. It did happen
      ignore = true;
    }

    @JdkConstants.InputEventMask
    int modifiers = e.getModifiers();
    @JdkConstants.InputEventMask
    int modifiersEx = e.getModifiersEx();
    if (e.getID() == MOUSE_PRESSED) {
      myPressedModifiersStored = true;
      myModifiers = modifiers;
      myModifiersEx = modifiersEx;
    }
    else if (e.getID() == MOUSE_RELEASED) {
      myForceTouchIsAllowed = true;
      if (myPressedModifiersStored) {
        myPressedModifiersStored = false;
        modifiers = myModifiers;
        modifiersEx = myModifiersEx;
      }
    }

    final JRootPane root = findRoot(e);
    if (root != null) {
      BlockState blockState = myRootPane2BlockedId.get(root);
      if (blockState != null) {
        if (SWING_EVENTS_PRIORITY.indexOf(blockState.currentEventId) < SWING_EVENTS_PRIORITY.indexOf(e.getID())) {
          blockState.currentEventId = e.getID();
          if (blockState.blockMode == IdeEventQueue.BlockMode.COMPLETE) {
            return true;
          }
          else {
            ignore = true;
          }
        } else {
          myRootPane2BlockedId.remove(root);
        }
      }
    }

    if (c == null) {
      throw new IllegalStateException("component cannot be null");
    }
    c = SwingUtilities.getDeepestComponentAt(c, e.getX(), e.getY());

    if (c instanceof IdeGlassPaneImpl) {
      c = ((IdeGlassPaneImpl)c).getTargetComponentFor(e);
    }

    if (c == null) { // do nothing if component doesn't contains specified point
      return false;
    }

    if (c instanceof MouseShortcutPanel || c.getParent() instanceof MouseShortcutPanel) {
      return false; // forward mouse processing to the special shortcut panel
    }

    if (isHorizontalScrolling(c, e)) {
      boolean done = doHorizontalScrolling(c, (MouseWheelEvent)e);
      if (done) return true;
    }

    if (ignore) return false;

    // avoid "cyclic component initialization error" in case of dialogs shown because of component initialization failure
    if (!KeymapManagerImpl.ourKeymapManagerInitialized) {
      return false;
    }

    final MouseShortcut shortcut = new MouseShortcut(button, modifiersEx, clickCount);
    fillActionsList(c, shortcut, IdeKeyEventDispatcher.isModalContext(c));
    ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
    if (actionManager != null) {
      AnAction[] actions = myActions.toArray(new AnAction[myActions.size()]);
      for (AnAction action : actions) {
        DataContext dataContext = DataManager.getInstance().getDataContext(c);
        Presentation presentation = myPresentationFactory.getPresentation(action);
        AnActionEvent actionEvent = new AnActionEvent(e, dataContext, ActionPlaces.MAIN_MENU, presentation,
                                                      ActionManager.getInstance(),
                                                      modifiers);
        if (ActionUtil.lastUpdateAndCheckDumb(action, actionEvent, false)) {
          actionManager.fireBeforeActionPerformed(action, dataContext, actionEvent);
          final Component context = PlatformDataKeys.CONTEXT_COMPONENT.getData(dataContext);

          if (context != null && !context.isShowing()) continue;

          ActionUtil.performActionDumbAware(action, actionEvent);
          actionManager.fireAfterActionPerformed(action, dataContext, actionEvent);
          e.consume();
        }
      }
    }
    return e.getButton() > 3;
  }

  private static void resetPopupTrigger(final MouseEvent e) {
    ReflectionUtil.setField(MouseEvent.class, e, boolean.class, "popupTrigger", false);
  }

  /**
   * This method patches event if it concerns side buttons like 4 (Backward) or 5 (Forward)
   * AND it's not single-click event. We won't support double-click for side buttons.
   * Also some JDK bugs produce zero-click events for side buttons.

   * @return true if event was patched
   */
  public static boolean patchClickCount(final MouseEvent e) {
    if (e.getClickCount() != 1 && e.getButton() > 3) {
      ReflectionUtil.setField(MouseEvent.class, e, int.class, "clickCount", 1);
      return true;
    }
    return false;
  }

  private boolean doHorizontalScrolling(Component c, MouseWheelEvent me) {
    final JScrollBar scrollBar = findHorizontalScrollBar(c);
    if (scrollBar != null) {
      if (scrollBar.hashCode() != myLastHorScrolledComponentHash) {
        FeatureUsageTracker.getInstance().triggerFeatureUsed("ui.horizontal.scrolling");
        myLastHorScrolledComponentHash = scrollBar.hashCode();
      }
      scrollBar.setValue(scrollBar.getValue() + getScrollAmount(me, scrollBar));
      return true;
    }
    return false;
  }

  public void resetHorScrollingTracker() {
    myLastHorScrolledComponentHash = 0;
  }

  private static int getScrollAmount(MouseWheelEvent me, JScrollBar scrollBar) {
    return me.getUnitsToScroll() * scrollBar.getUnitIncrement();
  }

  private static boolean isHorizontalScrolling(Component c, MouseEvent e) {
    if ( c != null
         && e instanceof MouseWheelEvent
         && (JDK_BUG_ID_8147994 || isDiagramViewComponent(c.getParent()))) {
      final MouseWheelEvent mwe = (MouseWheelEvent)e;
      return mwe.isShiftDown()
             && mwe.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL
             && JBScrollPane.isScrollEvent(mwe)
             && findHorizontalScrollBar(c) != null;
    }
    return false;
  }

  @Nullable
  private static JScrollBar findHorizontalScrollBar(Component c) {
    if (c == null) return null;
    if (c instanceof JScrollPane) {
      JScrollBar scrollBar = ((JScrollPane)c).getHorizontalScrollBar();
      return scrollBar != null && scrollBar.isVisible() ? scrollBar : null;
    }

    if (isDiagramViewComponent(c)) {
      final JComponent view = (JComponent)c;
      for (int i = 0; i < view.getComponentCount(); i++) {
         if (view.getComponent(i) instanceof JScrollBar) {
           final JScrollBar scrollBar = (JScrollBar)view.getComponent(i);
           if (scrollBar.getOrientation() == Adjustable.HORIZONTAL) {
             return scrollBar.isVisible() ? scrollBar : null;
           }
         }
      }
    }
    return findHorizontalScrollBar(c.getParent());
  }

  private static boolean isDiagramViewComponent(Component c) {
    return c != null && "y.view.Graph2DView".equals(c.getClass().getName());
  }

  public void blockNextEvents(@NotNull MouseEvent e, @NotNull IdeEventQueue.BlockMode blockMode) {
    final JRootPane root = findRoot(e);
    if (root == null) return;

    myRootPane2BlockedId.put(root, new BlockState(e.getID(), blockMode));
  }

  @Nullable
  private static JRootPane findRoot(MouseEvent e) {
    final Component parent = UIUtil.findUltimateParent(e.getComponent());
    JRootPane root = null;

    if (parent instanceof JWindow) {
      root = ((JWindow)parent).getRootPane();
    }
    else if (parent instanceof JDialog) {
      root = ((JDialog)parent).getRootPane();
    }
    else if (parent instanceof JFrame) {
      root = ((JFrame)parent).getRootPane();
    }

    return root;
  }

  private static class BlockState {
    private int currentEventId;
    private final IdeEventQueue.BlockMode blockMode;

    private BlockState(int id, IdeEventQueue.BlockMode mode) {
      currentEventId = id;
      blockMode = mode;
    }
  }
}
