/*
 * Copyright 2003-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 jetbrains.mps.ide.tools;

import com.intellij.ide.actions.ActivateToolWindowAction;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.KeyboardShortcut;
import com.intellij.openapi.actionSystem.Shortcut;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowAnchor;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactoryImpl;
import com.intellij.ui.content.ContentManager;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;

import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import java.awt.event.InputEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public abstract class BaseTool {
  private final static Logger LOG = LogManager.getLogger(BaseTool.class);

  private final Project myProject;
  private final String myId;
  private Icon myIcon;
  private Map<String, KeyStroke> myShortcutsByKeymap;
  private final ToolWindowAnchor myAnchor;
  private final boolean mySideTool;
  private boolean myCanCloseContent;
  private boolean myIsRegistered;
  private ToolWindowManager myWindowManager;

  private JComponent myComponent = null;

  @Deprecated
  @ToRemove(version = 3.5)
  public BaseTool(Project project, String id, int number, Icon icon, ToolWindowAnchor anchor, boolean canCloseContent) {
    this(project, id, number, icon, anchor, false, canCloseContent);
  }

  @Deprecated
  @ToRemove(version = 3.5)
  public BaseTool(Project project, String id, int number, Icon icon, ToolWindowAnchor anchor, boolean sideTool, boolean canCloseContent) {
    this(project, id, shortcutsFromNumber(number), icon, anchor, sideTool, canCloseContent);
  }

  protected static Map<String, KeyStroke> shortcutsFromNumber(int number) {
    Map<String, KeyStroke> result = new HashMap<>();
    if (number != -1) {
      result.put(KeymapManager.DEFAULT_IDEA_KEYMAP, KeyStroke.getKeyStroke("alt " + number));
      result.put(KeymapManager.MAC_OS_X_KEYMAP, KeyStroke.getKeyStroke("meta " + number));
    }
    return result;
  }

  public BaseTool(Project project, String id, Map<String, KeyStroke> shortcutsByKeymap, Icon icon, ToolWindowAnchor anchor, boolean sideTool,
      boolean canCloseContent) {
    myAnchor = anchor;
    mySideTool = sideTool;
    myShortcutsByKeymap = shortcutsByKeymap;
    myId = id;
    myIcon = icon;
    myCanCloseContent = canCloseContent;
    myProject = project;
    myIsRegistered = false;
  }

  public String getId() {
    return myId;
  }

  @Deprecated
  @ToRemove(version = 3.5)
  public int getNumber() {
    if (myShortcutsByKeymap != null) {
      KeyStroke defaultKeystroke = myShortcutsByKeymap.get(KeymapManager.DEFAULT_IDEA_KEYMAP);
      if (defaultKeystroke != null) {
        if (defaultKeystroke.getModifiers() == (InputEvent.ALT_MASK | InputEvent.ALT_DOWN_MASK)) {
          char keyChar = defaultKeystroke.getKeyChar();
          if (Character.isDigit(keyChar)) {
            return Character.digit(keyChar, 10);
          }
        }
      }
    }
    return -1;
  }

  public Icon getIcon() {
    return myIcon;
  }

  synchronized private boolean isRegistered() {
    return myIsRegistered;
  }

  synchronized private void setIsRegistered(boolean isRegistered) {
    myIsRegistered = isRegistered;
  }

  public boolean toolIsOpened() {
    ThreadUtils.assertEDT();
    return getToolWindow().isVisible();
  }

  /**
   * Runs {@link jetbrains.mps.ide.tools.BaseTool#openTool} later in EDT event pool.
   *
   * @param setActive determine if tool window must be just opened or additionally became active and attract focus
   */
  public void openToolLater(final boolean setActive) {
    ThreadUtils.runInUIThreadNoWait(() -> openTool(setActive));
  }

  /**
   * Opens the tool's window, shows tool if invisible at the moment.
   * Need to be called in EDT.
   *
   * @param setActive determine if tool window must be just opened or additionally became active and attract focus
   */
  public void openTool(boolean setActive) {
    ThreadUtils.assertEDT();
    ToolWindow window = getToolWindow();
    if (!isAvailable()) {
      makeAvailable();
    }
    if (!toolIsOpened()) {
      window.show(null);
    }
    if (setActive) {
      window.activate(null);
    }
  }

  /**
   * Runs {@link jetbrains.mps.ide.tools.BaseTool#close} later in EDT event pool.
   */
  // TODO: remove unused?
  public void closeLater() {
    ThreadUtils.runInUIThreadNoWait(this::close);
  }

  /**
   * Minimizes the window, doesn't remove tool from panel
   * Need to be called in EDT.
   */
  public void close() {
    ThreadUtils.assertEDT();
    if (isAvailable() && toolIsOpened()) {
      getToolWindow().hide(null);
    }
  }

  /**
   * @return whether the tool is visible by user (in the panel)
   */
  public boolean isAvailable() {
    ThreadUtils.assertEDT();
    return getToolWindow().isAvailable();
  }

  public void setAvailable(boolean state) {
    ThreadUtils.assertEDT();
    if (state) {
      makeAvailable();
    } else {
      makeUnavailable();
    }
  }

  /**
   * Runs {@link jetbrains.mps.ide.tools.BaseTool#makeAvailable} later in EDT event pool.
   */
  public void makeAvailableLater() {
    ThreadUtils.runInUIThreadNoWait(this::makeAvailable);
  }

  /**
   * If the tool is visible, does nothing, else show the tool in panel in minimized state
   */
  public void makeAvailable() {
    ThreadUtils.assertEDT();
    if (!isAvailable()) {
      getToolWindow().setAvailable(true, null);
    }
  }

  /**
   * Runs {@link jetbrains.mps.ide.tools.BaseTool#makeUnavailable} later in EDT event pool.
   */
  public void makeUnavailableLater() {
    ThreadUtils.runInUIThreadNoWait(this::makeUnavailable);
  }

  /**
   * Removes the tool from the panel
   */
  public void makeUnavailable() {
    ThreadUtils.assertEDT();
    if (isAvailable()) {
      getToolWindow().setAvailable(false, null);
    }
  }

  public ToolWindow getToolWindow() {
    ThreadUtils.assertEDT();

    if (!isRegistered()) {
      register();
    }
    // register() may fail if myProject hasn't been initialized - ToolWindowManager is a ProjectComponent
    return myWindowManager == null ? null : myWindowManager.getToolWindow(myId);
  }

  @NonNls
  @NotNull
  public String getComponentName() {
    return getClass().getName();
  }

  public void registerLater() {
    ThreadUtils.runInUIThreadNoWait(() -> DumbService.getInstance(getProject()).runWhenSmart(this::register));
  }

  public final void register() {
    if (myProject.isDisposed()) {
      return;
    }
    if (isRegistered()) {
      return;
    }
    ThreadUtils.assertEDT();
    setIsRegistered(true);

    myWindowManager = ToolWindowManager.getInstance(myProject);

    if (myShortcutsByKeymap != null) {
      String actionId = ActivateToolWindowAction.getActionIdForToolWindow(myId);

      List<Keymap> keymaps = new ArrayList<>(myShortcutsByKeymap.size());
      for (Entry<String, KeyStroke> keymapItem : myShortcutsByKeymap.entrySet()) {
        Keymap keymap = KeymapManager.getInstance().getKeymap(keymapItem.getKey());
        if (keymap == null) {
          LOG.warn("Keymap " + keymapItem.getKey() + " cannot be found");
          return;
        }
        keymaps.add(keymap);
      }
      // keymaps topsort here is needed because we need to remove inherited shortcuts if they are overwritten
      Collections.sort(keymaps, new Comparator<Keymap>() {
        @Override
        public int compare(Keymap o1, Keymap o2) {
          for (Keymap parent = o1.getParent(); parent != null; parent = parent.getParent()) {
            if (parent.equals(o2)) {
              return 1;
            }
          }
          for (Keymap parent = o2.getParent(); parent != null; parent = parent.getParent()) {
            if (parent.equals(o1)) {
              return -1;
            }
          }
          return 0;
        }
      });

      for (Keymap keymap : keymaps) {
        KeyboardShortcut defShortcut = new KeyboardShortcut(myShortcutsByKeymap.get(keymap.getName()), null);
        keymap.removeAllActionShortcuts(actionId);
        keymap.addShortcut(actionId, defShortcut);
      }
    }

    //if we create a new project, tool windows are created for it automatically
    ToolWindow toolWindow = myWindowManager.getToolWindow(myId);
    if (toolWindow == null) {
      toolWindow = myWindowManager.registerToolWindow(myId, myCanCloseContent, myAnchor, getProject(), true, mySideTool);
    }
    toolWindow.setIcon(myIcon);

    toolWindow.setToHideOnEmptyContent(true);
    toolWindow.installWatcher(toolWindow.getContentManager());
    setAvailable(isInitiallyAvailable());

    doRegister();

    if (myComponent == null) {
      myComponent = getComponent();
    }
    if (myComponent != null) {
      addContent(myComponent, "", null, false);
    }

    toolWindow.setToHideOnEmptyContent(true);
    toolWindow.installWatcher(toolWindow.getContentManager());
    setAvailable(isInitiallyAvailable());
  }

  /**
   * Override this method to add implementation specific registration.
   * Called in {@link jetbrains.mps.ide.tools.BaseTool#register} before {@link jetbrains.mps.ide.tools.BaseTool#getComponent()}.
   */
  protected void doRegister() {

  }

  public int getCurrentTabIndex() {
    ContentManager contentManager = getContentManager();
    return contentManager.getIndexOfContent(contentManager.getSelectedContent());
  }

  protected AnAction createCloseAction() {
    return new CloseAction(this);
  }

  protected boolean isInitiallyAvailable() {
    return false;
  }


  /**
   * Runs {@link jetbrains.mps.ide.tools.BaseTool#unregister instead} later in EDT event pool.
   */
  // TODO: remove unused?
  public void unregisterLater() {
    ThreadUtils.runInUIThreadNoWait(this::unregister);
  }

  /**
   * Unregister Tool and removes all shortcuts in case of reload.
   * Need to be called in EDT.
   * <p>
   * If project is closing (== not in opened projects) {
   * <p>
   * }, but there are some other opened projects,
   * than shortcuts must not be removed - instance of BaseTool still exists for other projects
   * and shortcuts are global (registered by ActionId).
   * In case of BaseTool reload (unregister on opened project) we need do this,
   * because it will register (probably changed) shortcuts back on load.
   */
  public final void unregister() {
    if (!isRegistered()) {
      return;
    }
    ThreadUtils.assertEDT();

    doUnregister();

    // see Javadoc for if condition explanation
    final List<Project> openedProjects = Arrays.asList(ProjectManager.getInstance().getOpenProjects());
    if (myShortcutsByKeymap != null && (openedProjects.contains(getProject()) || openedProjects.isEmpty())) {
      for (Entry<String, KeyStroke> keymapItem : myShortcutsByKeymap.entrySet()) {
        Keymap keymap = KeymapManager.getInstance().getKeymap(keymapItem.getKey());
        if (keymap != null) {
          keymap.removeAllActionShortcuts(ActivateToolWindowAction.getActionIdForToolWindow(myId));
        }
      }
    }

    ToolWindow toolWindow = getToolWindow();
    if (toolWindow != null) {
      ContentManager contentManager = toolWindow.getContentManager();
      if (contentManager != null && !contentManager.isDisposed()) {
        contentManager.removeAllContents(true);
      }
    }

    myWindowManager.unregisterToolWindow(myId);
    myIsRegistered = false;
  }

  /**
   * Override this method to add implementation specific unregistration.
   * Called in {@link jetbrains.mps.ide.tools.BaseTool#unregister} before default unregister process.
   */
  protected void doUnregister() {

  }

  // TODO: make method abstract - fix jetbrains.mps.ide.findusages.view.UsagesViewTool
  public JComponent getComponent() {
    return null;
  }

  protected Content addContent(JComponent component, @NotNull String name, Icon icon, boolean isLockable) {
    Content content = new ContentFactoryImpl().createContent(component, name, isLockable);
    if (icon != null) {
      content.putUserData(ToolWindow.SHOW_CONTENT_ICON, Boolean.TRUE);
      content.setIcon(icon);
    } else {
      content.setIcon(myIcon);
    }
    ContentManager contentManager = getContentManager();
    contentManager.addContent(content);
    return content;
  }

  public void setSelectedComponent(JComponent component) {
    ContentManager manager = getContentManager();
    Content content = manager.getContent(component);
    manager.setSelectedContent(content);
  }

  protected ContentManager getContentManager() {
    if (!isRegistered()) {
      register();
    }
    if (getToolWindow() == null) {
      return null;
    }
    return getToolWindow().getContentManager();
  }

  @Override
  public String toString() {
    return "Tool " + this.getComponentName();
  }

  protected Project getProject() {
    return myProject;
  }

  public void init(Project project) {
  }

  public void dispose() {

  }
}
