/*
 * Copyright 2000-2017 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.preview.impl;

import com.intellij.icons.AllIcons;
import com.intellij.ide.ui.UISettings;
import com.intellij.ide.ui.UISettingsListener;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.preview.PreviewInfo;
import com.intellij.openapi.preview.PreviewManager;
import com.intellij.openapi.preview.PreviewPanelProvider;
import com.intellij.openapi.preview.PreviewProviderId;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.wm.*;
import com.intellij.openapi.wm.impl.ToolWindowImpl;
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl;
import com.intellij.openapi.wm.impl.content.ToolWindowContentUi;
import com.intellij.ui.Gray;
import com.intellij.ui.JBColor;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentManager;
import com.intellij.ui.content.ContentManagerAdapter;
import com.intellij.ui.content.ContentManagerEvent;
import com.intellij.util.Alarm;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.util.*;
@State(name = "PreviewManager", storages = @Storage(StoragePathMacros.WORKSPACE_FILE))
public class PreviewManagerImpl implements PreviewManager, PersistentStateComponent<PreviewManagerState> {
  private static final Key<PreviewInfo> INFO_KEY = Key.create("preview_info");
  private static final int HISTORY_LIMIT = 10;

  private final Project myProject;
  private final Alarm myAlarm = new Alarm();

  private ToolWindowImpl myToolWindow;

  private ContentManager myContentManager;
  private Content myEmptyStateContent;
  private final JPanel myEmptyStatePanel;

  private ArrayList<PreviewInfo> myHistory = new ArrayList<>();


  private TreeSet<PreviewPanelProvider> myProviders = new TreeSet<>((o1, o2) -> {
    int result = Float.compare(o1.getMenuOrder(), o2.getMenuOrder());
    return result != 0 ? result : o1.toString().compareTo(o2.toString());
  });
  private Set<PreviewProviderId> myActiveProviderIds = new HashSet<>();
  private Set<PreviewProviderId> myLockedProviderIds = new HashSet<>();
  private boolean myInnerSelectionChange;

  private static boolean isAvailable() {
    return UISettings.getInstance().getNavigateToPreview();
  }

  public PreviewManagerImpl(Project project) {
    myProject = project;
    myEmptyStatePanel = new EmptyStatePanel();
    PreviewPanelProvider[] providers = PreviewPanelProvider.EP_NAME.getExtensions(project);
    for (PreviewPanelProvider provider : providers) {
      myProviders.add(provider);
      myActiveProviderIds.add(provider.getId());
      Disposer.register(project, provider);
    }

    project.getMessageBus().connect().subscribe(UISettingsListener.TOPIC, uiSettings -> checkGlobalState());
    checkGlobalState();
    checkEmptyState();
  }

  @Nullable
  @Override
  public PreviewManagerState getState() {
    PreviewManagerState state = new PreviewManagerState();
    state.myArtifactFilesMap = new HashMap<>();
    for (PreviewPanelProvider provider : myProviders) {
      state.myArtifactFilesMap.put(provider.toString(), myActiveProviderIds.contains(provider.getId()));
    }
    return state;
  }

  @Override
  public void loadState(PreviewManagerState state) {
    if (state == null) return;
    for (Map.Entry<String, Boolean> entry : state.myArtifactFilesMap.entrySet()) {
      if (!entry.getValue()) {
        for (Iterator<PreviewProviderId> iterator = myActiveProviderIds.iterator(); iterator.hasNext(); ) {
          PreviewProviderId id = iterator.next();
          if (id.getVisualName().equals(entry.getKey())) {
            iterator.remove();
            break;
          }
        }
      }
    }
  }

  @Nullable
  private <V, C> PreviewPanelProvider<V, C> findProvider(@NotNull PreviewProviderId<V, C> id) {
    for (PreviewPanelProvider provider : myProviders) {
      if (id == provider.getId()) return provider;
    }
    return null;
  }


  protected void checkGlobalState() {
    ToolWindowManagerImpl toolWindowManager = (ToolWindowManagerImpl)ToolWindowManager.getInstance(myProject);
    if (!isAvailable() && toolWindowManager.getToolWindow(ToolWindowId.PREVIEW) != null) {
      myHistory.clear();
      myContentManager.removeAllContents(true);
      toolWindowManager.unregisterToolWindow(ToolWindowId.PREVIEW);
      return;
    }
    if (isAvailable() && toolWindowManager.getToolWindow(ToolWindowId.PREVIEW) == null) {
      myToolWindow = (ToolWindowImpl)toolWindowManager
        .registerToolWindow(ToolWindowId.PREVIEW, myEmptyStatePanel, ToolWindowAnchor.RIGHT, myProject, false);
      myContentManager = myToolWindow.getContentManager();
      myToolWindow.setIcon(AllIcons.Toolwindows.ToolWindowPreview);
      myToolWindow.setContentUiType(ToolWindowContentUiType.COMBO, null);
      myToolWindow.setAutoHide(true);
      myEmptyStateContent = myContentManager.getContent(0);
      final MoveToStandardViewAction moveToStandardViewAction = new MoveToStandardViewAction();
      myContentManager.addContentManagerListener(new ContentManagerAdapter() {
        @Override
        public void selectionChanged(ContentManagerEvent event) {
          if (myInnerSelectionChange || event.getOperation() != ContentManagerEvent.ContentOperation.add) return;
          PreviewInfo previewInfo = event.getContent().getUserData(INFO_KEY);
          if (previewInfo != null) {
            preview(previewInfo, false);
            myToolWindow.setTitleActions(previewInfo.supportsStandardPlace() ? moveToStandardViewAction : null);
          }
        }
      });

      moveToStandardViewAction.registerCustomShortcutSet(new ShortcutSet() {
        @NotNull
        @Override
        public Shortcut[] getShortcuts() {
          Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
          return keymap.getShortcuts("ShowContent");
        }
      }, myToolWindow.getComponent());

      myToolWindow.setTitleActions(moveToStandardViewAction);
      ArrayList<AnAction> myGearActions = new ArrayList<>();
      for (PreviewPanelProvider provider : myProviders) {
        myGearActions.add(new ContentTypeToggleAction(provider));
      }
      myToolWindow.setAdditionalGearActions(new DefaultActionGroup("Preview", myGearActions));
      myToolWindow.activate(() -> myToolWindow.activate(null));
    }
  }

  private void checkEmptyState() {
    if (myContentManager.getContents().length == 0) {
      myToolWindow.getComponent().putClientProperty(ToolWindowContentUi.HIDE_ID_LABEL, "false");
      myContentManager.addContent(myEmptyStateContent);
    }
    else if (myContentManager.getContents().length > 1) {
      myToolWindow.getComponent().putClientProperty(ToolWindowContentUi.HIDE_ID_LABEL, "true");
      myContentManager.removeContent(myEmptyStateContent, false);
    }
  }

  @Nullable
  private Content getContent(@NotNull PreviewInfo info) {
    for (Content content : myContentManager.getContents()) {
      PreviewInfo eachInfo = content.getUserData(INFO_KEY);
      if (info.equals(eachInfo)) return content;
    }
    return null;
  }

  @NotNull
  private Content addContent(PreviewInfo info) {
    myHistory.add(info);
    while (myHistory.size() > HISTORY_LIMIT) {
      close(myHistory.remove(0));
    }

    Content content = myContentManager.getFactory().createContent(info.getComponent(), info.getTitle(), false);

    myContentManager.addContent(content, 0);
    checkEmptyState();
    return content;
  }

  private static void updateContentWithInfo(Content content, PreviewInfo info) {
    content.putUserData(ToolWindow.SHOW_CONTENT_ICON, Boolean.TRUE);
    content.putUserData(INFO_KEY, info);
    content.setIcon(info.getIcon());
    content.setPopupIcon(info.getIcon());
  }

  private void close(@NotNull PreviewInfo info) {
    Content content = getContent(info);
    if (content != null) {
      myContentManager.removeContent(content, false);
      info.release();
      if (myContentManager.getContents().length == 0) {
        toggleToolWindow(false, null);
      }
      checkEmptyState();
    }
  }

  @Override
  public <V, C> C preview(@NotNull PreviewProviderId<V, C> id, V data, boolean requestFocus) {
    if (!myActiveProviderIds.contains(id) || myLockedProviderIds.contains(id)) {
      return null;
    }
    PreviewPanelProvider<V, C> provider = findProvider(id);
    if (provider == null) {
      return null;
    }
    return preview(PreviewInfo.create(provider, data), requestFocus);
  }

  private <V, C> C preview(@NotNull final PreviewInfo<V, C> info, boolean requestFocus) {
    toggleToolWindow(true, null);
    Content content = getContent(info);
    Content selectedContent = myContentManager.getSelectedContent();
    if (selectedContent != content) {
      myInnerSelectionChange = true;
      try {
        PreviewInfo selectedInfo = selectedContent != null ? selectedContent.getUserData(INFO_KEY) : null;
        if (selectedInfo != null && selectedInfo.isModified(selectedInfo.getId() == info.getId())) {
          moveToStandardPlaceImpl(selectedInfo.getId(), selectedInfo.getData());
        }
        if (content == null) {
          content = addContent(info);
        }
      }
      finally {
        myInnerSelectionChange = false;
      }
    }
    if (content != null) {
      myContentManager.addContent(content, 0);//Adjust usage order
    }
    myInnerSelectionChange = true;
    try {
      if (content != null) {
        updateContentWithInfo(content, info);
        myContentManager.setSelectedContent(content, requestFocus);
      }
      return info.initComponent(requestFocus);
    }
    finally {
      myInnerSelectionChange = false;
    }
  }

  @Override
  public <V, C> void close(@NotNull PreviewProviderId<V, C> id, V data) {
    for (Content content : myContentManager.getContents()) {
      PreviewInfo info = content.getUserData(INFO_KEY);
      if (info != null && info.getId() == id && info.getProvider().contentsAreEqual(info.getData(),data)) {
        close(info);
        break;
      }
    }
  }

  @Override
  public <V, C> void moveToStandardPlaceImpl(@NotNull PreviewProviderId<V, C> id, V data) {
    PreviewPanelProvider<V, C> provider = findProvider(id);
    if (provider == null) return;
    myLockedProviderIds.add(id);
    try {
      provider.showInStandardPlace(data);
    } finally {
      myLockedProviderIds.remove(id);
    }
    close(id, data);
  }

  private void toggleToolWindow(boolean activate, Runnable runnable) {
    final ToolWindow toolWindow = ToolWindowManager.getInstance(myProject).getToolWindow(ToolWindowId.PREVIEW);
    if (toolWindow != null && activate != toolWindow.isActive()) {
      if (activate) {
        toolWindow.activate(runnable, true);
      }
      else {
        if (!myAlarm.isEmpty()) {
          toolWindow.hide(null);
        }
      }
    }
  }

  private class MoveToStandardViewAction extends AnAction {

    public MoveToStandardViewAction() {
      super("Move to standard view", "Move to standard view", AllIcons.Actions.MoveToStandardPlace);
    }

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
      Content selectedContent = myContentManager.getSelectedContent();
      if (selectedContent == null) return;
      PreviewInfo previewInfo = selectedContent.getUserData(INFO_KEY);
      if (previewInfo != null) {
        moveToStandardPlaceImpl(previewInfo.getId(), previewInfo.getData());
        toggleToolWindow(false, null);
      }
    }
  }

  private class ContentTypeToggleAction extends ToggleAction {
    private final PreviewPanelProvider myProvider;

    ContentTypeToggleAction(PreviewPanelProvider provider) {
      super(provider.getId().getVisualName());
      myProvider = provider;
    }

    @Override
    public boolean isSelected(AnActionEvent e) {
      return myActiveProviderIds.contains(myProvider.getId());
    }

    @Override
    public void setSelected(AnActionEvent e, boolean state) {
      if (state) {
        myActiveProviderIds.add(myProvider.getId());
      }
      else {
        myActiveProviderIds.remove(myProvider.getId());
        for (Iterator<PreviewInfo> iterator = myHistory.iterator(); iterator.hasNext(); ) {
          PreviewInfo info = iterator.next();
          if (info.getId().equals(myProvider.getId())) {
            Content content = getContent(info);
            if (content != null) {
              myContentManager.removeContent(content, true);
            }
            iterator.remove();
          }
        }
        checkEmptyState();
      }
    }
  }

  private static class EmptyStatePanel extends JPanel {
    public EmptyStatePanel() {
      setOpaque(true);
    }

    @Override
    public void paint(Graphics g) {
      boolean isDarkBackground = UIUtil.isUnderDarcula();
      UISettings.setupAntialiasing(g);
      g.setColor(new JBColor(isDarkBackground ? Gray._230 : Gray._80, Gray._160));
      g.setFont(JBUI.Fonts.label(isDarkBackground ? 24f : 20f));

      UIUtil.TextPainter painter = new UIUtil.TextPainter().withLineSpacing(1.5f);
      painter.withShadow(true, new JBColor(Gray._200.withAlpha(100), Gray._0.withAlpha(255)));

      painter.appendLine("No files are open");//.underlined(new JBColor(Gray._150, Gray._180));
      painter.draw(g, (width, height) -> {
        Dimension s = this.getSize();
        return Couple.of((s.width - width) / 2, (s.height - height) / 2);
      });
    }
  }
}
