/*
 * 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.messages;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentManager;
import com.intellij.ui.content.MessageView;
import com.intellij.ui.content.MessageView.SERVICE;
import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.ide.ThreadUtils.RunInUIRunnable;
import jetbrains.mps.ide.messages.MessageList.MessageListState;
import jetbrains.mps.ide.messages.MessagesViewTool.MessageViewToolState;
import jetbrains.mps.ide.messages.navigation.NavigationManager;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.messages.IMessage;
import jetbrains.mps.messages.IMessageHandler;
import jetbrains.mps.messages.Message;
import jetbrains.mps.messages.MessageKind;
import org.jetbrains.annotations.NotNull;

import javax.swing.JList;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@State(
    name = "MessagesViewTool",
    storages = @Storage(StoragePathMacros.WORKSPACE_FILE)
)
public class MessagesViewTool implements ProjectComponent, PersistentStateComponent<MessageViewToolState>, Disposable {
  private static final String DEFAULT_LIST = "DEFAULT_LIST";

  private final Project myProject;
  private final NavigationManager myNavigationManager;
  private final ToolWindowManager myToolWindowManager;
  private final MyMessageList myDefaultList;
  private final Map<Object, List<MessageList>> myMessageLists = new HashMap<Object, List<MessageList>>();
  private final MessageViewLoggingHandler myMessageViewLoggingHandler;

  public MessagesViewTool(Project project, NavigationManager navigationManager, ToolWindowManager toolWindowManager) {
    myProject = project;
    myNavigationManager = navigationManager;
    myToolWindowManager = toolWindowManager;
    myDefaultList = new MyMessageList("Messages");
    Disposer.register(this, myDefaultList);
    // default list doesn't need too much attention, don't activate it on any message
    myDefaultList.setActivateOnMessage(false);
    myDefaultList.setTitleUpdateFormat(
        "{1,choice,0#--|1#1 error|2#{1} errors}/{2,choice,0#--|1#1 warning|2#{2} warnings}/{3,choice,0#--|1#1 info|2#{3} infos}");
    addList(DEFAULT_LIST, myDefaultList);
    myMessageViewLoggingHandler = new MessageViewLoggingHandler(this, ProjectHelper.fromIdeaProject(project));
  }

  @Override
  public void dispose() {
    myMessageLists.clear();
  }

  public Project getProject() {
    return myProject;
  }

  public void clear() {
    getDefaultList().clear();
  }

  public void clear(String listName) {
    MessageList list = getAvailableList(listName, false);
    if (list != null) {
      list.clear();
    }
  }

  public void add(final IMessage message) {
    getDefaultList().add(message);
  }

  public void add(final IMessage message, String listName) {
    getAvailableList(listName, true).add(message);
  }

  @Override
  @NotNull
  public String getComponentName() {
    return MessagesViewTool.class.getSimpleName();
  }

  @Override
  public void initComponent() {
    getDefaultList().createContent(false, false);
  }

  @Override
  public void disposeComponent() {
    Disposer.dispose(this);
  }

  @Override
  public void projectOpened() {
    myMessageViewLoggingHandler.register();
  }

  @Override
  public void projectClosed() {
    myMessageViewLoggingHandler.unregister();
  }

  public static class MessageViewToolState {
    public MessageViewToolState(MessageListState defaultListState) {
      this.defaultListState = defaultListState;
    }

    public MessageViewToolState() {
      // default cons is essential for IDEA to construct the state.
      this.defaultListState = new MessageListState();
    }

    // XXX documentation of PersistentStateComponent tells it saves annotated and public fields. This one is neither of those.
    //     I don't see MessagesViewTool entry in .mps/workspace.xml either. Does it work?
    MessageListState defaultListState;
  }

  @Override
  public MessageViewToolState getState() {
    return new MessageViewToolState(getDefaultList().getState());
  }

  @Override
  public void loadState(MessageViewToolState state) {
    getDefaultList().loadState(state.defaultListState);
  }

  /*package*/ MessageView getMessagesService() {
    return SERVICE.getInstance(myProject);
  }

  public IMessageHandler newHandler() {
    return new MsgHandler(getDefaultList());
  }

  /**
   * Shorthand for {@link #newHandler(String, boolean) newHandler(name, false)}.
   */
  public IMessageHandler newHandler(@NotNull final String name) {
    return newHandler(name, false /*newHandler used to keep the list intact in MPS releases to date*/);
  }

  /**
   * @param name identifies named collection of messages, value is visible in UI as the name of the collection.
   * @param clear true if caller doesn't need to keep messages already reported under same named handler (if any)
   * @return handler that pipes messages to designated UI component, never {@code null}
   */
  public IMessageHandler newHandler(@NotNull final String name, boolean clear) {
    MessageList availableList = getAvailableList(name, true);
    if (clear) {
      availableList.clear();
    }
    return new MsgHandler(availableList);
  }

  private synchronized void addList(String name, MessageList list) {
    List<MessageList> lists = myMessageLists.containsKey(name) ? myMessageLists.get(name) : new ArrayList<MessageList>();
    if (!myMessageLists.containsKey(name)) {
      myMessageLists.put(name, lists);
    }
    lists.add(list);
  }

  /**
   * @deprecated I don't feel it's a nice idea to expose implementation detail, {@link #newHandler()} shall suffice, perhaps,
   *             augmented with options object to pass info/warn/clear settings
   */
  @Deprecated
  public synchronized MessageList getAvailableList(String name, boolean createIfNotFound) {
    List<MessageList> lists;
    if (myMessageLists.containsKey(name)) {
      lists = myMessageLists.get(name);
    } else {
      myMessageLists.put(name, lists = new ArrayList<MessageList>());
    }
    for (int i = lists.size() - 1; i >= 0; --i) {
      MessageList messageList = lists.get(i);
      if (!messageList.isPinned()) {
        return messageList;
      }
    }
    if (createIfNotFound) {
      MessageList list = createList(name);
      lists.add(list);
      return list;
    }
    return null;
  }

  private synchronized MessageList createList(String name) {
    MyMessageList list = new MyMessageList(name);
    Disposer.register(this, list);
    list.loadState(getDefaultList().getState());
    list.setActivateOnMessage(true);
    list.createContent(true, true);
    return list;
  }

  /*package*/
  synchronized void removeList(MessageList list, String name) {
    List<MessageList> lists = myMessageLists.get(name);
    if (lists != null) {
      lists.remove(list);
    }
  }

  private MessageList getDefaultList() {
    return myDefaultList;
  }

  private class MyMessageList extends MessageList {
    private final String myTitle;
    private String myTitleUpdateFormat =
        "{0}: {1,choice,0#--|1#1 error|2#{1} errors}/{2,choice,0#--|1#1 warning|2#{2} warnings}/{3,choice,0#--|1#1 info|2#{3} infos}";
    /*
     * getMessagesService().getContentManager() may fail with NPE as respective tool window is initialized as post-startup activity
     * while users are quite quick to run rebuild (or another messages client). One way to prevent NPE is to check
     * {@code myToolWindowManager.getToolWindow(ToolWindowId.MESSAGES_WINDOW) != null}, another is to track whether our createContent() completed.
     */
    private boolean myContentReady = false;

    protected MyMessageList(@NotNull String title) {
      myTitle = title;
    }

    @Override
    public void dispose() {
      myContentReady = false;
      super.dispose();
      removeList(this, myTitle);
    }

    @Override
    protected void bringToFront() {
      ToolWindow window = myToolWindowManager.getToolWindow(ToolWindowId.MESSAGES_WINDOW);
      if (window == null) {
        return; // just in case
      }
      if (!window.isAvailable()) {
        window.setAvailable(true, null);
      }
      if (!window.isVisible()) {
        window.show(null);
      }
      Content content = getMessagesService().getContentManager().getContent(getComponent());
      getMessagesService().getContentManager().setSelectedContent(content);
    }

    public void setTitleUpdateFormat(String pattern) {
      myTitleUpdateFormat = pattern;
    }

    public void createContent(final boolean canClose, final boolean isMultiple) {
      if (RuntimeFlags.isTestMode()) {
        return;
      }

      final Runnable initRunnable = new Runnable() {
        @Override
        public void run() {
          initUI();
          final MessageView service = getMessagesService();
          Content content = service.getContentManager().getFactory().createContent(getComponent(), myTitle, true);

          content.setCloseable(canClose);
          content.setPinnable(isMultiple);
          if (canClose) {
            content.setShouldDisposeContent(true);
            content.setDisposer(MyMessageList.this);
          }
          content.putUserData(ToolWindow.SHOW_CONTENT_ICON, Boolean.TRUE);
          service.getContentManager().addContent(content);
          activateUpdate();
          myContentReady = true;
        }
      };
      getMessagesService().runWhenInitialized(new RunInUIRunnable(initRunnable, false));
    }

    @Override
    public boolean isPinned() {
      if (myContentReady) {
        ContentManager contentManager = getMessagesService().getContentManager();
        Content content = contentManager != null ? contentManager.getContent(getComponent()) : null;
        return content != null && content.isPinned();
      }
      return super.isPinned();
    }

    @Override
    protected void updateHeader() {
      if (myTitle.equals(myTitleUpdateFormat) || myTitleUpdateFormat == null) {
        return;
      }
      Content content = getMessagesService().getContentManager().getContent(getComponent());
      if (content != null) {
        if (hasErrors() || hasWarnings() || hasInfo()) {
          final String t = MessageFormat.format(myTitleUpdateFormat, myTitle, myErrors, myWarnings, myInfos);
          content.setDisplayName(t);
        } else {
          content.setDisplayName(myTitle);
        }
      }
    }

    @Override
    protected boolean canNavigate(@NotNull IMessage message) {
      return message.getHintObject() != null && myNavigationManager.canNavigateTo(message.getHintObject());
    }

    @Override
    protected void navigate(@NotNull IMessage message, boolean focus) {
      // XXX could receive Navigatable from NM and in case navigation fails (e.g. due to deleted/missing node)
      // could show a balloon with explanation (it's better to do it here rather than in Navigatable itself as here we've
      // got tool window to anchor.
      myNavigationManager.navigateTo(message.getHintObject(), focus);
    }

    @Override
    protected void populateActions(JList list, DefaultActionGroup group) {
      ActionGroup acts = (ActionGroup) ActionManager.getInstance().getAction("MPS.MessagesView");
      group.addAll(acts);
    }
  }

  public static void log(Project p, MessageKind kind, String message) {
    p.getComponent(MessagesViewTool.class).add(new Message(kind, message));
  }

  private static class MsgHandler implements IMessageHandler {
    private final MessageList myList;

    MsgHandler(@NotNull MessageList list) {
      myList = list;
    }

    @Override
    public void handle(@NotNull IMessage msg) {
      myList.add(msg);
    }
  }
}
