/*
Copyright 2011-2016 Google Inc. All Rights Reserved.

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.google.security.zynamics.zylib.gui.zygraph;

import com.google.security.zynamics.zylib.general.ListenerProvider;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.IZyRegenerateableRealizer;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CAbstractKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CBackspaceKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CCharKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CCopyKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CCursorKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CCutKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CDelKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CEndKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CHomeKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CInsertKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CPasteKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CRedoKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CReturnKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CSelectAllKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CTabKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.CUndoKeyBehavior;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.KeyBehaviours.UndoHistroy.CUndoManager;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.ZyCaret;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.ZyLabelContent;
import com.google.security.zynamics.zylib.yfileswrap.gui.zygraph.AbstractZyGraph;

import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Map;

public class CDefaultLabelEventHandler extends KeyAdapter {
  private final AbstractZyGraph<?, ?> m_graph;

  private final Map<Integer, CAbstractKeyBehavior> m_ctrlKeyBehaviourMap =
      new HashMap<Integer, CAbstractKeyBehavior>();
  private final Map<Integer, CAbstractKeyBehavior> m_keyBehaviourMap =
      new HashMap<Integer, CAbstractKeyBehavior>();

  private final CUndoManager m_undoManager = new CUndoManager();

  private ZyLabelContent m_activeLabelContent;

  private IZyRegenerateableRealizer m_activeRealizer;

  private final InternalFocusListener m_focusListener = new InternalFocusListener();

  private final ListenerProvider<ILabelEditableContentListener> m_editModeListener =
      new ListenerProvider<ILabelEditableContentListener>();

  public CDefaultLabelEventHandler(final AbstractZyGraph<?, ?> graph) {
    m_graph = graph;

    m_graph.addViewFocusListener(m_focusListener);

    init();
  }

  private String getContentSnippet(final int yPos) {
    String snippet = "";

    int firstLineIndex = getActiveLabelContent().getFirstLineIndexOfModelAt(yPos);
    int lastLineIndex = getActiveLabelContent().getLastLineIndexOfModelAt(yPos);

    for (int lineIndex = firstLineIndex; lineIndex <= lastLineIndex; ++lineIndex) {
      snippet += getActiveLabelContent().getLineContent(lineIndex).getText();
    }

    if (firstLineIndex > 0) {
      lastLineIndex = firstLineIndex - 1;
      firstLineIndex = getActiveLabelContent().getFirstLineIndexOfModelAt(lastLineIndex);

      String content = "";
      for (int lineIndex = firstLineIndex; lineIndex <= lastLineIndex; ++lineIndex) {
        content += getActiveLabelContent().getLineContent(lineIndex).getText();
      }

      snippet = content + snippet;
    }

    lastLineIndex = getActiveLabelContent().getLastLineIndexOfModelAt(yPos);
    if ((lastLineIndex + 1) < (getActiveLabelContent().getLineCount() - 1)) {
      firstLineIndex = lastLineIndex + 1;
      lastLineIndex = getActiveLabelContent().getLastLineIndexOfModelAt(firstLineIndex);

      for (int lineIndex = firstLineIndex; lineIndex <= lastLineIndex; ++lineIndex) {
        snippet += getActiveLabelContent().getLineContent(lineIndex).getText();
      }
    }

    return snippet;
  }

  private void init() {
    m_keyBehaviourMap.put(KeyEvent.VK_UP, new CCursorKeyBehavior(m_undoManager));
    m_keyBehaviourMap.put(KeyEvent.VK_DOWN, new CCursorKeyBehavior(m_undoManager));
    m_keyBehaviourMap.put(KeyEvent.VK_LEFT, new CCursorKeyBehavior(m_undoManager));
    m_keyBehaviourMap.put(KeyEvent.VK_RIGHT, new CCursorKeyBehavior(m_undoManager));
    m_keyBehaviourMap.put(KeyEvent.VK_TAB, new CTabKeyBehavior(m_undoManager)); // jump to next/prev
                                                                                // editable object
    m_keyBehaviourMap.put(KeyEvent.VK_HOME, new CHomeKeyBehavior(m_undoManager));
    m_keyBehaviourMap.put(KeyEvent.VK_END, new CEndKeyBehavior(m_undoManager));
    m_keyBehaviourMap.put(KeyEvent.VK_INSERT, new CInsertKeyBehavior(m_undoManager)); // copy //
                                                                                      // Ctrl-INS //
                                                                                      // paste //
                                                                                      // Shift-INS
    m_keyBehaviourMap.put(KeyEvent.VK_DELETE, new CDelKeyBehavior(m_undoManager)); // cut //
                                                                                   // SHIFT-Del
    m_keyBehaviourMap.put(KeyEvent.VK_BACK_SPACE, new CBackspaceKeyBehavior(m_undoManager));
    m_keyBehaviourMap.put(KeyEvent.VK_ENTER, new CReturnKeyBehavior(m_undoManager));
    m_keyBehaviourMap.put(null, new CCharKeyBehavior(m_undoManager));
    m_ctrlKeyBehaviourMap.put(KeyEvent.VK_A, new CSelectAllKeyBehavior(m_undoManager)); // select
                                                                                        // all //
                                                                                        // Ctrl-A
    m_ctrlKeyBehaviourMap.put(KeyEvent.VK_X, new CCutKeyBehavior(m_undoManager)); // cut // Ctrl-X
    m_ctrlKeyBehaviourMap.put(KeyEvent.VK_C, new CCopyKeyBehavior(m_undoManager)); // copy // Ctrl-C
    m_ctrlKeyBehaviourMap.put(KeyEvent.VK_V, new CPasteKeyBehavior(m_undoManager)); // paste //
                                                                                    // Ctrl-V
    m_ctrlKeyBehaviourMap.put(KeyEvent.VK_Y, new CRedoKeyBehavior(m_undoManager)); // redo // ctrl-Y
    m_ctrlKeyBehaviourMap.put(KeyEvent.VK_Z, new CUndoKeyBehavior(m_undoManager)); // undo // ctrl-Z
  }

  protected AbstractZyGraph<?, ?> getGraph() {
    return m_graph;
  }

  public void activateLabelContent(final ZyLabelContent labelContent,
      final IZyRegenerateableRealizer activeRealizer) {
    if ((labelContent == null) || (activeRealizer == null)) {
      return; // should not happen
    }
    unregisterListener();

    if ((getActiveLabelContent() != null) && (labelContent != getActiveLabelContent())) {
      if (getActiveLabelContent().isSelectable()) {
        getActiveLabelContent().showCaret(false);

        m_graph.updateViews();
      }
    }

    setActiveLabelContent(labelContent);
    m_activeRealizer = activeRealizer;
    getActiveLabelContent().showCaret(getActiveLabelContent().isSelectable());

    m_undoManager.setLabelContent(getActiveLabelContent());

    registerListener();
  }

  public void addEditModeListener(final ILabelEditableContentListener listener) {
    m_editModeListener.addListener(listener);
  }

  public void addKeyBehaviour(final Integer keyCode, final CAbstractKeyBehavior behaviour,
      final boolean ctrl) {
    if (ctrl) {
      m_ctrlKeyBehaviourMap.put(keyCode, behaviour);
    } else {
      m_keyBehaviourMap.put(keyCode, behaviour);
    }
  }

  public void deactivateLabelContent() {
    unregisterListener();

    if ((getActiveLabelContent() != null) && getActiveLabelContent().isSelectable()) {
      getActiveLabelContent().showCaret(false);

      setActiveLabelContent(null);
      m_activeRealizer = null;
    }

    m_graph.updateViews();

    m_undoManager.setLabelContent(getActiveLabelContent());

    registerListener();
  }

  public void dispose() {
    m_graph.removeViewFocusListener(m_focusListener);
  }

  public void handleMouseDraggedEvent(final double labelParentX, final double labelParentY,
      final double mouseX, final double mouseY, final double zoomFactor) {
    final ZyCaret caret = getActiveLabelContent().getCaret();

    final int oldlr = caret.getYmouseReleased();
    final int oldpr = caret.getXmouseReleased();

    caret.setCaretEnd(labelParentX, labelParentY, mouseX, mouseY, zoomFactor);

    if ((caret.getYmouseReleased() != oldlr) || (caret.getXmouseReleased() != oldpr)) {
      m_activeRealizer.repaint();
    }
  }

  public void handleMousePressedEvent(final double labelParentX, final double labelParentY,
      final double mouseX, final double mouseY, final double zoomFactor) {
    final ZyCaret caret = getActiveLabelContent().getCaret();

    caret.setCaretStart(labelParentX, labelParentY, mouseX, mouseY, zoomFactor);
    caret.setCaretEnd(labelParentX, labelParentY, mouseX, mouseY, zoomFactor);

    m_activeRealizer.repaint();
  }

  public void handleMouseReleasedEvent(final double labelParentX,
      final double labelParentY,
      final double mouseX,
      final double mouseY,
      final double zoomFactor,
      final int clickCount) {
    final ZyCaret caret = getActiveLabelContent().getCaret();

    switch (clickCount) {
      case 1:
        caret.setCaretEnd(labelParentX, labelParentY, mouseX, mouseY, zoomFactor);
        break;
      case 2:
        caret.selectWord(labelParentX, labelParentY, mouseX, mouseY, zoomFactor);
        break;
      case 3:
        caret.selectLine(labelParentY, mouseY);
        break;
      case 4:
        caret.selectAll();
    }

    m_activeRealizer.repaint();
  }

  public boolean hasEmptySelection() {
    return getActiveLabelContent().getSelectedText().equals("");
  }

  public boolean isActive() {
    return getActiveLabelContent() != null;
  }

  public boolean isActiveLabel(final ZyLabelContent labelContent) {
    return (labelContent != null) && (labelContent == getActiveLabelContent());
  }

  @Override
  public void keyPressed(final KeyEvent event) {
    if (!isActive()) {
      return;
    }

    if (event.getKeyCode() == KeyEvent.VK_ESCAPE) {
      deactivateLabelContent();

      return;
    }

    if ((event.getKeyCode() == KeyEvent.VK_TAB) && (event.getModifiers() != 0)
        && (event.getModifiers() != InputEvent.SHIFT_DOWN_MASK)) {
      return;
    }

    if (!getActiveLabelContent().isSelectable()) {
      return;
    }

    CAbstractKeyBehavior behaviour = null;

    final char keyText = KeyEvent.getKeyText(event.getKeyCode()).charAt(0);

    if (event.isControlDown() && (keyText >= 'A') && (keyText <= 'Z')) {
      behaviour = m_ctrlKeyBehaviourMap.get(event.getKeyCode());
    }

    if (behaviour == null) {
      behaviour = m_keyBehaviourMap.get(event.getKeyCode());

      if ((behaviour == null) && (event.getKeyChar() != KeyEvent.CHAR_UNDEFINED)
          && !event.isControlDown() && getActiveLabelContent().isEditable()) {
        behaviour = m_keyBehaviourMap.get(null);
      }
    }

    if (behaviour != null) {
      int y = -1;
      String oldContent = "";

      if (getActiveLabelContent().isEditable()) {
        y = getActiveLabelContent().getCaret().getYmouseReleased();
        oldContent = getContentSnippet(y);
      }

      behaviour.keyPressed(getActiveLabelContent(), event);

      if (y > -1) {
        final String newContent = getContentSnippet(y);

        if (!oldContent.equals(newContent) && !oldContent.isEmpty()) {
          for (final ILabelEditableContentListener listener : m_editModeListener) {
            listener.editableContentChanged(getActiveLabelContent());
          }
        }
      }

      m_activeRealizer.regenerate();

      m_activeRealizer.repaint();

      m_graph.updateViews();
    }

    event.consume();
  }

  public void registerListener() {
    if (isActive()) {
      m_graph.addViewCanvasKeyListener(this);
    }
  }

  public void removeEditModeListener(final ILabelEditableContentListener listener) {
    m_editModeListener.removeListener(listener);
  }

  public void unregisterListener() {
    if (isActive()) {
      m_graph.removeViewCanvasKeyListener(this);
    }
  }

  private ZyLabelContent getActiveLabelContent() {
    return m_activeLabelContent;
  }

  private void setActiveLabelContent(ZyLabelContent m_activeLabelContent) {
    this.m_activeLabelContent = m_activeLabelContent;
  }

  private class InternalFocusListener implements FocusListener {
    @Override
    public void focusGained(final FocusEvent e) {}

    @Override
    public void focusLost(final FocusEvent e) {
      deactivateLabelContent();
    }
  }
}
