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

import com.intellij.util.containers.SortedList;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.nodeEditor.inspector.InspectorEditorComponent;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCell_Collection;
import jetbrains.mps.openapi.editor.message.EditorMessageOwner;
import jetbrains.mps.openapi.editor.message.SimpleEditorMessage;
import jetbrains.mps.openapi.editor.update.UpdaterListener;
import jetbrains.mps.openapi.editor.update.UpdaterListenerAdapter;
import jetbrains.mps.util.containers.ManyToManyMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.module.ModelAccess;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;


public class NodeHighlightManager implements EditorMessageOwner {
  private static final Comparator<SimpleEditorMessage> EDITOR_MESSAGES_COMPARATOR = new Comparator<SimpleEditorMessage>() {
    @Override
    public int compare(SimpleEditorMessage m1, SimpleEditorMessage m2) {
      if (m1.getPriority() != m2.getPriority()) {
        return m1.getPriority() - m2.getPriority();
      }
      return m1.getStatus().ordinal() - m2.getStatus().ordinal();
    }
  };

  // TODO: replace myMessagesLock usages with this ?
  private final Object myMessagesLock = new Object();

  @NotNull
  private final EditorComponent myEditor;
  private final Set<SimpleEditorMessage> myMessages = new HashSet<SimpleEditorMessage>();
  private final Map<EditorMessageOwner, Set<SimpleEditorMessage>> myOwnerToMessages = new HashMap<EditorMessageOwner, Set<SimpleEditorMessage>>();
  private final ManyToManyMap<SimpleEditorMessage, SNode> myMessagesToNodes = new ManyToManyMap<SimpleEditorMessage, SNode>();

  /**
   * All caches are synchronized using myMessagesLock
   */
  private Map<EditorCell, List<SimpleEditorMessage>> myMessagesCache = Collections.emptyMap();
  private volatile boolean myRebuildMessagesCache = false;
  private final UpdaterListener myRebuildListener = new RebuildMessagesOnEditorUpdate();
  private Set<EditorMessageIconRenderer> myIconRenderersCache = new HashSet<EditorMessageIconRenderer>();
  private volatile boolean myRebuildIconRenderersCacheFlag = false;
  private boolean myDisposed = false;

  public NodeHighlightManager(@NotNull EditorComponent editor) {
    myEditor = editor;
    myEditor.getUpdater().addListener(myRebuildListener);
  }

  /**
   * scheduling lazy rebuild of myMessagesCache and myIconRenderersCache
   * this method can be called from any thread
   * this method should be called inside synchronize(myMessagesLock) block only
   */
  private void invalidateMessagesCaches() {
    myRebuildMessagesCache = true;
    myRebuildIconRenderersCacheFlag = true;
  }

  private Map<EditorCell, List<SimpleEditorMessage>> getMessagesCache() {
    synchronized (myMessagesLock) {
      return myMessagesCache;
    }
  }

  private void refreshMessagesCache() {
    assert ThreadUtils.isInEDT() : "refreshMessagesCache() should be called from EDT only";
    assert getModelAccess().canRead() : "refreshMessagesCache() should be called inside model read action only";
    synchronized (myMessagesLock) {
      if (!myRebuildMessagesCache) {
        return;
      }

      myRebuildMessagesCache = false;
      if (myMessages.isEmpty() || myEditor.getRootCell() == null) {
        myMessagesCache = Collections.emptyMap();
      } else {
        myMessagesCache = new HashMap<EditorCell, List<SimpleEditorMessage>>();
        rebuildMessages(myEditor.getRootCell());
      }
    }
  }

  private ModelAccess getModelAccess() {
    return myEditor.getRepository().getModelAccess();
  }

  /**
   * part of myMessagesCache rebuild process
   * this method should be called inside synchronize(myMessagesLock) block only
   */
  private void rebuildMessages(EditorCell root) {
    List<SimpleEditorMessage> messages = calculateMessages(root);
    if (!messages.isEmpty()) {
      myMessagesCache.put(root, messages);
    }

    if (root instanceof EditorCell_Collection) {
      for (EditorCell cell : (EditorCell_Collection) root) {
        rebuildMessages(cell);
      }
    }
  }

  public List<SimpleEditorMessage> getMessages(EditorCell cell) {
    assert !myDisposed;
    List<SimpleEditorMessage> result = getMessagesCache().get(cell);
    if (result != null) {
      return new ArrayList<SimpleEditorMessage>(result);
    }
    return Collections.emptyList();
  }

  /**
   * part of myMessagesCache rebuild process
   * this method should be called inside synchronize(myMessagesLock) block only
   */
  private List<SimpleEditorMessage> calculateMessages(EditorCell cell) {
    final SNode node = cell.getSNode();
    if (node == null) {
      return Collections.emptyList();
    }

    final List<SimpleEditorMessage> result = new SortedList<SimpleEditorMessage>(EDITOR_MESSAGES_COMPARATOR);
    Set<SimpleEditorMessage> messageSet = myMessagesToNodes.getBySecond(node);
    for (SimpleEditorMessage message : messageSet) {
      if (!(message instanceof EditorMessage) || ((EditorMessage) message).acceptCell(cell, myEditor)) {
        result.add(message);
      }
    }
    if (myEditor.getRootCell() != cell || !(myEditor instanceof InspectorEditorComponent)) {
      // the condition above is because an inspector for the node
      // does not have cells for some node's children (they are edited in main editor)
      // but the cell should not be highlighted only because of this
      if (cell.isBig()) {
        for (SNode child : node.getChildren()) {
          EditorCell cellForChild = myEditor.findNodeCell(child);
          if (cellForChild == null) {
            getMessagesFromDescendants(child, result);
          }
        }
      }
    }
    return result;
  }

  private void getMessagesFromDescendants(SNode nodeWithoutCell, List<SimpleEditorMessage> messages) {
    messages.addAll(myMessagesToNodes.getBySecond(nodeWithoutCell));
    for (SNode child : nodeWithoutCell.getChildren()) {
      EditorCell cellForChild = myEditor.findNodeCell(child);
      if (cellForChild == null) {
        getMessagesFromDescendants(child, messages);
      }
    }
  }

  private void addMessage(SimpleEditorMessage m) {
    if (m.getNode() == null) {
      return;
    }

    EditorMessageOwner owner = m.getOwner();
    if (!myOwnerToMessages.containsKey(owner)) {
      myOwnerToMessages.put(owner, new HashSet<SimpleEditorMessage>());
    }
    myOwnerToMessages.get(owner).add(m);
    myMessages.add(m);

    myMessagesToNodes.addLink(m, m.getNode());
  }

  private boolean removeMessage(SimpleEditorMessage m) {
    if (m == null) {
      return false;
    }
    EditorMessageOwner owner = m.getOwner();
    Set<SimpleEditorMessage> messages = myOwnerToMessages.get(owner);
    if (messages != null) {
      messages.remove(m);
      if (messages.isEmpty()) {
        myOwnerToMessages.remove(owner);
      }
    }
    myMessages.remove(m);
    if (myEditor.hasUI()) {
      myEditor.getMessagesGutter().remove(m);
    }

    myMessagesToNodes.clearFirst(m);
    return true;
  }

  public void mark(SimpleEditorMessage message) {
    for (SimpleEditorMessage msg : getMessages()) {
      if (msg.sameAs(message)) return;
    }

    synchronized (myMessagesLock) {
      addMessage(message);
      invalidateMessagesCaches();
    }
    if (message.showInGutter() && myEditor.hasUI()) {
      myEditor.getMessagesGutter().add(message);
    }
  }

  public void unmark(SimpleEditorMessage message) {
    synchronized (myMessagesLock) {
      if (removeMessage(message)) {
        invalidateMessagesCaches();
      }
    }
  }

  public boolean clearForOwner(EditorMessageOwner owner) {
    return clearForOwner(owner, true);
  }

  public boolean hasMessages(EditorMessageOwner owner) {
    synchronized (myMessagesLock) {
      return myOwnerToMessages.containsKey(owner);
    }
  }

  public boolean clearForOwner(EditorMessageOwner owner, boolean repaintAndRebuild) {
    boolean result = myEditor.getMessagesGutter().removeMessages(owner);
    synchronized (myMessagesLock) {
      if (myOwnerToMessages.containsKey(owner)) {
        ArrayList<SimpleEditorMessage> messages = new ArrayList<SimpleEditorMessage>(myOwnerToMessages.get(owner));
        for (SimpleEditorMessage m : messages) {
          removeMessage(m);
        }
        invalidateMessagesCaches();
      }
    }
    if (repaintAndRebuild) {
      repaintAndRebuildEditorMessages();
    }
    return result;
  }

  /**
   * perform refresh of messages visible in LeftEditorHighlighter
   * and repaint associated EditorComponent
   */
  public void repaintAndRebuildEditorMessages() {
    getModelAccess().runReadInEDT(new Runnable() {
      @Override
      public void run() {
        if (myDisposed) {
          return;
        }
        refreshMessagesCache();
        if (myEditor.hasUI()) {
          refreshLeftHighlighterMessages();
          myEditor.repaintExternalComponent();
        }
      }
    });
  }

  private void refreshLeftHighlighterMessages() {
    assert ThreadUtils.isInEDT() : "refreshLeftHighlighterMessages() should be called from EDT only";
    Set<EditorMessageIconRenderer> oldIconRenderers;
    Set<EditorMessageIconRenderer> newIconRenderers;
    synchronized (myMessagesLock) {
      if (!myRebuildIconRenderersCacheFlag) {
        return;
      }
      myRebuildIconRenderersCacheFlag = false;
      oldIconRenderers = myIconRenderersCache;
      newIconRenderers = myIconRenderersCache = new HashSet<EditorMessageIconRenderer>();
      for (SimpleEditorMessage message : myMessages) {
        if (message instanceof EditorMessageIconRenderer) {
          myIconRenderersCache.add((EditorMessageIconRenderer) message);
        }
      }
    }
    myEditor.getLeftEditorHighlighter().removeAllIconRenderers(oldIconRenderers);
    myEditor.getLeftEditorHighlighter().addAllIconRenderers(newIconRenderers);
  }

  public void mark(SNode node, Color color, String messageText, EditorMessageOwner owner) {
    if (node == null) return;
    mark(new DefaultEditorMessage(node, color, messageText, owner));
  }

  public void mark(List<? extends SimpleEditorMessage> messages) {
    for (SimpleEditorMessage message : messages) {
      mark(message);
    }
    repaintAndRebuildEditorMessages();
  }

  /**
   * Should work even if NodeHighlightManager is disposed because it can be called by the Highlighter thread
   */
  public Set<SimpleEditorMessage> getMessages() {
    Set<SimpleEditorMessage> result = new HashSet<SimpleEditorMessage>();
    synchronized (myMessagesLock) {
      result.addAll(myMessages);
    }
    return result;
  }

  public List<SimpleEditorMessage> getMessagesFor(SNode node) {
    List<SimpleEditorMessage> result = new ArrayList<SimpleEditorMessage>();
    synchronized (myMessagesLock) {
      result.addAll(myMessagesToNodes.getBySecond(node));
    }
    return result;
  }

  public List<SimpleEditorMessage> getMessagesFor(SNode node, EditorMessageOwner owner) {
    List<SimpleEditorMessage> result = new ArrayList<SimpleEditorMessage>();
    synchronized (myMessagesLock) {
      for (SimpleEditorMessage message : myMessagesToNodes.getBySecond(node)) {
        if (message.getOwner() == owner) {
          result.add(message);
        }
      }
    }
    return result;
  }

  public void dispose() {
    assert ThreadUtils.isInEDT() : "dispose() should be called from EDT only";
    myDisposed = true;
    myEditor.getUpdater().removeListener(myRebuildListener);
  }

  private class RebuildMessagesOnEditorUpdate extends UpdaterListenerAdapter {
    @Override
    public void editorUpdated(jetbrains.mps.openapi.editor.EditorComponent editorComponent) {
      assert !myDisposed;
      if (needRebuild()) {
        invalidateMessagesCaches();
        repaintAndRebuildEditorMessages();
      }
    }

    private boolean needRebuild() {
      if (getMessagesCache().isEmpty()) {
        return true;
      }

      for (EditorCell cell : getMessagesCache().keySet()) {
        if (!myEditor.isValid(cell)) {
          return true;
        }
      }

      return false;
    }
  }
}
