/*
 * Copyright 2003-2015 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.bookmark;

import com.intellij.icons.AllIcons;
import com.intellij.ide.bookmarks.Bookmark;
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.ui.LightColors;
import jetbrains.mps.ide.bookmark.BookmarkManager.MyState;
import jetbrains.mps.nodeEditor.Highlighter;
import jetbrains.mps.openapi.navigation.EditorNavigator;
import jetbrains.mps.project.MPSProject;
import jetbrains.mps.util.Pair;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;

import javax.swing.Icon;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;


@State(
  name = "MPSBookmarkManager",
  storages = @Storage(StoragePathMacros.WORKSPACE_FILE)
)
public class BookmarkManager implements ProjectComponent, PersistentStateComponent<MyState> {
  private static final Logger LOG = LogManager.getLogger(BookmarkManager.class);

  private static Icon myUnnumberedBookmarkIcon = AllIcons.Actions.Checked;

  private List<BookmarkListener> myBookmarkListeners = new ArrayList<BookmarkListener>();

  private SNodeReference[] myBookmarks = new SNodeReference[10];

  private List<SNodeReference> myUnnumberedBookmarks = new ArrayList<SNodeReference>();

  private final MPSProject myProject;
  private Highlighter myHighlighter;
  private BookmarksHighlighter myChecker;

  public BookmarkManager(MPSProject project, Highlighter highlighter) {
    myProject = project;
    myHighlighter = highlighter;
  }

  @Override
  public void projectOpened() {
  }

  @Override
  public void projectClosed() {
  }

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

  @Override
  public void initComponent() {
    myChecker = new BookmarksHighlighter(this);
    myHighlighter.addChecker(myChecker);
  }

  @Override
  public void disposeComponent() {
    myHighlighter.removeChecker(myChecker);
    myChecker.dispose();
  }

  public List<Pair<SNode, Integer>> getBookmarks(SNode root) {
    if (root == null) return Collections.emptyList();
    List<Pair<SNode, Integer>> result = new ArrayList<Pair<SNode, Integer>>();
    for (int i = 0; i <= 9; i++) {
      SNodeReference nodePointer = myBookmarks[i];
      if (nodePointer != null) {
        SNode node = nodePointer.resolve(myProject.getRepository());
        if (node != null && node.getContainingRoot() == root) {
          result.add(new Pair<SNode, Integer>(node, i));
        }
      }
    }
    for (SNodeReference nodePointer : myUnnumberedBookmarks) {
      if (nodePointer != null) {
        SNode node = nodePointer.resolve(myProject.getRepository());
        if (node != null && node.getContainingRoot() == root) {
          result.add(new Pair<SNode, Integer>(node, -1));
        }
      }
    }
    return result;
  }

  public void setUnnumberedBookmark(SNode node) {
    if (node == null) {
      LOG.error("node to bookmark is null");
      return;
    }
    SNodeReference newBookmark = node.getReference();
    boolean bookmarkRemoved = false;
    for (int i = 0; i < 10; i++) {
      if (myBookmarks[i] != null && myBookmarks[i].resolve(myProject.getRepository()) == node) {
        myBookmarks[i] = null;
        bookmarkRemoved = true;
        fireBookmarkRemoved(i, node);
      }
    }
    if (myUnnumberedBookmarks.contains(newBookmark)) {
      myUnnumberedBookmarks.remove(newBookmark);
      bookmarkRemoved = true;
      fireBookmarkRemoved(-1, newBookmark.resolve(myProject.getRepository()));
    }
    if (!bookmarkRemoved) {
      myUnnumberedBookmarks.add(newBookmark);
      fireBookmarkAdded(-1, newBookmark.resolve(myProject.getRepository()));
    }
  }

  public void setBookmark(SNode node, int number) {
    if (node == null) {
      LOG.error("node to bookmark is null");
      return;
    }
    if (number == -1) {
      setUnnumberedBookmark(node);
      return;
    }

    SNodeReference newBookmark = new jetbrains.mps.smodel.SNodePointer(node);

    for (int i = 0; i < 10; i++) {
      SNodeReference bookmark = myBookmarks[i];
      if (i != number && bookmark != null && bookmark.resolve(myProject.getRepository()) == node) {
        return;
      }
    }
    if (getAllUnnumberedBookmarks().contains(newBookmark)) {
      return;
    }

    SNodeReference oldBookmark = myBookmarks[number];
    SNode oldNode = null;
    myBookmarks[number] = null;
    if (oldBookmark != null) {
      oldNode = oldBookmark.resolve(myProject.getRepository());
      fireBookmarkRemoved(number, oldNode);
    }
    if (!node.equals(oldNode)) {
      myBookmarks[number] = newBookmark;
      fireBookmarkAdded(number, node);
    }
  }

  public void clearBookmarks() {
    for (int i = 0; i < myBookmarks.length; i++) {
      SNodeReference pointer = myBookmarks[i];
      if (pointer != null) {
        myBookmarks[i] = null;
        fireBookmarkRemoved(i, pointer.resolve(myProject.getRepository()));
      }
    }
    ArrayList<SNodeReference> nodePointers = new ArrayList<SNodeReference>(myUnnumberedBookmarks);
    myUnnumberedBookmarks.clear();
    for (SNodeReference pointer : nodePointers) {
      if (pointer != null) {
        fireBookmarkRemoved(-1, pointer.resolve(myProject.getRepository()));
      }
    }
  }

  public void removeBookmark(int i) {
    if (i > 9) return;
    SNodeReference pointer = myBookmarks[i];
    if (pointer != null) {
      myBookmarks[i] = null;
      fireBookmarkRemoved(i, pointer.resolve(myProject.getRepository()));
    }
  }

  public void removeUnnumberedBookmark(SNodeReference nodePointer) {
    if (myUnnumberedBookmarks.contains(nodePointer)) {
      myUnnumberedBookmarks.remove(nodePointer);
      fireBookmarkRemoved(-1, nodePointer.resolve(myProject.getRepository()));
    }
  }

  public List<SNodeReference> getAllBookmarks() {
    List<SNodeReference> nodePointers = getAllNumberedBookmarks();
    nodePointers.addAll(getAllUnnumberedBookmarks());
    return nodePointers;
  }

  public List<SNodeReference> getAllNumberedBookmarks() {
    return Arrays.asList(myBookmarks);
  }

  public List<SNodeReference> getAllUnnumberedBookmarks() {
    return new ArrayList<SNodeReference>(myUnnumberedBookmarks);
  }

  public static Icon getIcon(int bookmarkNumber) {
    if (bookmarkNumber == -1) {
      return myUnnumberedBookmarkIcon;
    }
    return new MnemonicIcon(Character.forDigit(bookmarkNumber, 10));
  }

  public SNodeReference getBookmark(int number) {
    return myBookmarks[number];
  }

  public void navigateToBookmark(int number) {
    if (number < 0 || number > 9) return;
    SNodeReference pointer = myBookmarks[number];
    if (pointer == null) {
      return;
    }
    new EditorNavigator(myProject).shallFocus(true).shallSelect(true).open(pointer);
  }

  public void addBookmarkListener(BookmarkListener listener) {
    myBookmarkListeners.add(listener);
  }

  public void removeBookmarkListener(BookmarkListener listener) {
    myBookmarkListeners.remove(listener);
  }

  public boolean hasBookmarkListener(BookmarkListener listener) {
    return myBookmarkListeners.contains(listener);
  }

  private void fireBookmarkAdded(int number, SNode node) {
    for (BookmarkListener listener : myBookmarkListeners) {
      listener.bookmarkAdded(number, node);
    }
  }

  private void fireBookmarkRemoved(int number, SNode node) {
    for (BookmarkListener listener : myBookmarkListeners) {
      listener.bookmarkRemoved(number, node);
    }
  }

  @Override
  public MyState getState() {
    MyState state = new MyState();
    for (int i = 0; i < myBookmarks.length; i++) {
      SNodeReference pointer = myBookmarks[i];
      if (pointer != null) {
        state.myBookmarkInfos[i] = new BookmarkInfo(pointer, i);
      } else {
        state.myBookmarkInfos[i] = new BookmarkInfo();
      }
    }
    state.myUnnumberedBookmarkInfos = new BookmarkInfo[myUnnumberedBookmarks.size()];
    for (int i = 0; i < myUnnumberedBookmarks.size(); i++) {
      SNodeReference pointer = myUnnumberedBookmarks.get(i);
      if (pointer != null) {
        state.myUnnumberedBookmarkInfos[i] = new BookmarkInfo(pointer, -1);
      } else {
        state.myUnnumberedBookmarkInfos[i] = new BookmarkInfo();
      }
    }
    return state;
  }

  @Override
  public void loadState(MyState state) {
    for (int i = 0; i < state.myBookmarkInfos.length; i++) {
      BookmarkInfo bookmarkInfo = state.myBookmarkInfos[i];
      if (!bookmarkInfo.myIsNull) {
        assert i == bookmarkInfo.myNumber;
        myBookmarks[i] = bookmarkInfo.myNodeRef;
      } else {
        myBookmarks[i] = null;
      }
    }
    myUnnumberedBookmarks.clear();
    for (BookmarkInfo bookmarkInfo : state.myUnnumberedBookmarkInfos) {
      if (bookmarkInfo != null) {
        myUnnumberedBookmarks.add(bookmarkInfo.myNodeRef);
      }
    }
  }

  public interface BookmarkListener {
    public void bookmarkAdded(int number, SNode node);

    public void bookmarkRemoved(int number, SNode node);
  }

  public static class MyState {
    public BookmarkInfo[] myBookmarkInfos = new BookmarkInfo[10];
    public BookmarkInfo[] myUnnumberedBookmarkInfos = new BookmarkInfo[0];
  }

  public static class BookmarkInfo {
    private SNodeReference myNodeRef;
    public int myNumber;
    public boolean myIsNull = true;

    public BookmarkInfo() {
      myIsNull = true;
    }

    public BookmarkInfo(SNodeReference nodeRef, int number) {
      myNodeRef = nodeRef;
      myNumber = number;
      myIsNull = false;
    }

    //for serialization/deserialization
    @SuppressWarnings("UnusedDeclaration")
    public String getNodeRef() {
      if (myNodeRef == null) return "";
      return jetbrains.mps.smodel.SNodePointer.serialize(myNodeRef);
    }

    //for serialization/deserialization
    @SuppressWarnings("UnusedDeclaration")
    public void setNodeRef(String nodeRef) {
      if (nodeRef.equals("")) return;
      myNodeRef = jetbrains.mps.smodel.SNodePointer.deserialize(nodeRef);
    }
  }

  private static class MnemonicIcon implements Icon {
    private final char myMnemonic;

    private MnemonicIcon(char mnemonic) {
      myMnemonic = mnemonic;
    }

    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
      g.setColor(LightColors.YELLOW);
      g.fillRect(x, y, getIconWidth(), getIconHeight());

      g.setColor(Color.gray);
      g.drawRect(x, y, getIconWidth(), getIconHeight());

      g.setColor(Color.black);
      final Font oldFont = g.getFont();
      g.setFont(Bookmark.getBookmarkFont());

      g.drawString(Character.toString(myMnemonic), x + 2, y + getIconHeight() - 2);
      g.setFont(oldFont);
    }

    @Override
    public int getIconWidth() {
      return 10;
    }

    @Override
    public int getIconHeight() {
      return 12;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      MnemonicIcon that = (MnemonicIcon) o;

      return myMnemonic == that.myMnemonic;
    }
  }
}
