// Copyright 2012 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.collide.client.code.gotodefinition;

import org.waveprotocol.wave.client.common.util.SignalEvent;
import org.waveprotocol.wave.client.common.util.SignalEvent.KeyModifier;
import org.waveprotocol.wave.client.common.util.UserAgent;

import collide.client.filetree.FileTreeModel;

import com.google.collide.client.code.popup.EditorPopupController;
import com.google.collide.client.codeunderstanding.CubeClient;
import com.google.collide.client.documentparser.DocumentParser;
import com.google.collide.client.editor.Buffer;
import com.google.collide.client.editor.Editor;
import com.google.collide.client.editor.input.CommonActions;
import com.google.collide.client.editor.input.DefaultActionExecutor;
import com.google.collide.client.editor.input.InputScheme;
import com.google.collide.client.editor.input.RootActionExecutor;
import com.google.collide.client.editor.input.Shortcut;
import com.google.collide.client.editor.selection.SelectionModel;
import com.google.collide.client.history.Place;
import com.google.collide.client.util.PathUtil;
import com.google.collide.shared.document.LineFinder;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.collide.shared.util.StringUtils;
import com.google.common.base.Preconditions;
import com.google.gwt.event.dom.client.KeyCodes;

import elemental.events.Event;

/**
 * Implementation of the "go to definition" feature.
 *
 */
public class GoToDefinitionHandler extends DefaultActionExecutor {

  private static final int COMMAND_KEY_CODE = 91;

  private final Editor editor;
  private final FileTreeModel fileTreeModel;
  private final GoToDefinitionRenderer referenceRenderer;

  private final Shortcut gotoDefinitionAction = new Shortcut() {
    @Override
    public boolean event(InputScheme scheme, SignalEvent event) {
      goToDefinitionAtCurrentCaretPosition();
      return true;
    }
  };

  private LineFinder lineFinder;

  /**
   * Flag which specifies that object is "activated".
   *
   * <p>Activated object plugs renderer and listens to mouse to highlight
   * hovered items.
   */
  private boolean activated;
  private ListenerRegistrar.Remover mouseMoveListenerRemover;
  private ListenerRegistrar.Remover mouseClickListenerRemover;
  private ListenerRegistrar.Remover nativeKeyUpListenerRemover;

  /**
   * Flag which specifies that object is "attached" to editor.
   *
   * <p>Registered object listens for keypeseesd (to become activated) and
   * provides actions.
   */
  private boolean registered;
  private ListenerRegistrar.Remover keyListenerRemover;
  private RootActionExecutor.Remover actionRemover;

  private ReferenceStore referenceStore;
  private ReferenceNavigator referenceNavigator;
  private AnchorTagParser anchorParser;

  private static boolean isActionOnlyKey(SignalEvent signal) {
    // When modifier key is pressed on Mac it is instantly applied to modifiers.
    // So we check on Mac that only "CMD" is pressed.
    // On non-Mac when "CTRL" is the first key in sequence, there are no
    // modifiers applied yet.
    KeyModifier modifier = UserAgent.isMac() ? KeyModifier.META : KeyModifier.NONE;
    return modifier.check(signal) && (signal.getKeyCode() == getActionKeyCode());
  }

  private static int getActionKeyCode() {
    return UserAgent.isMac() ? COMMAND_KEY_CODE : KeyCodes.KEY_CTRL;
  }

  private final Buffer.MouseMoveListener mouseMoveListener = new Buffer.MouseMoveListener() {
    @Override
    public void onMouseMove(int x, int y) {
      NavigableReference reference = findReferenceAtMousePosition(x, y, false);
      referenceRenderer.highlightReference(reference, editor.getRenderer(), lineFinder);
    }
  };

  private final Buffer.MouseClickListener mouseClickListener = new Buffer.MouseClickListener() {
    @Override
    public void onMouseClick(int x, int y) {
      NavigableReference reference = findReferenceAtMousePosition(x, y, true);
      if (reference != null) {
        navigateReference(reference);
      }
    }
  };

  private final Editor.NativeKeyUpListener keyUpListener = new Editor.NativeKeyUpListener() {
    @Override
    public boolean onNativeKeyUp(Event event) {
      com.google.gwt.user.client.Event gwtEvent = (com.google.gwt.user.client.Event) event;
      if (gwtEvent.getKeyCode() == getActionKeyCode()) {
        setActivated(false);
        return true;
      } else {
        return false;
      }
    }
  };

  private final Editor.KeyListener keyListener = new Editor.KeyListener() {

    @Override
    public boolean onKeyPress(SignalEvent signal) {
      boolean shouldActivate = isActionOnlyKey(signal);
      setActivated(shouldActivate);
      return shouldActivate;
    }
  };

  public GoToDefinitionHandler(Place currentPlace, Editor editor, FileTreeModel fileTreeModel,
      GoToDefinitionRenderer.Resources resources, CubeClient cubeClient,
      EditorPopupController popupController) {
    Preconditions.checkNotNull(editor);
    Preconditions.checkNotNull(cubeClient);

    this.editor = editor;
    this.fileTreeModel = fileTreeModel;
    this.referenceRenderer = new GoToDefinitionRenderer(resources, editor, popupController);
    this.referenceNavigator = new ReferenceNavigator(currentPlace, editor);

    addAction(CommonActions.GOTO_DEFINITION, gotoDefinitionAction);
    addAction(CommonActions.GOTO_SOURCE, gotoDefinitionAction);

    referenceStore = new ReferenceStore(cubeClient);
  }

  public void editorContentsReplaced(PathUtil filePath, DocumentParser parser) {
    setActivated(false);
    if (anchorParser != null) {
      anchorParser.cleanup();
      anchorParser = null;
    }
    DynamicReferenceProvider dynamicReferenceProvider = null;
    if (isGoToDefinitionSupported(filePath)) {
      if (StringUtils.endsWithIgnoreCase(filePath.getPathString(), ".html")) {
        anchorParser = new AnchorTagParser(parser);
      }
      DeferringLineParser deferringParser = new DeferringLineParser(parser);
      dynamicReferenceProvider = new DynamicReferenceProvider(
          filePath.getPathString(), deferringParser, fileTreeModel, anchorParser);
      this.referenceNavigator.setCurrentFilePath(filePath.getPathString());
      this.lineFinder = editor.getDocument().getLineFinder();
      setRegistered(true);
    } else {
      setRegistered(false);
    }
    this.referenceStore.onDocumentChanged(editor.getDocument(), dynamicReferenceProvider);
  }

  /**
   * Registers or unregisters keyListener, actions, etc.
   */
  private void setRegistered(boolean registered) {
    if (registered == this.registered) {
      return;
    }
    if (registered) {
      keyListenerRemover = editor.getKeyListenerRegistrar().add(keyListener);
      actionRemover = editor.getInput().getActionExecutor().addDelegate(this);
    } else {
      keyListenerRemover.remove();
      keyListenerRemover = null;
      actionRemover.remove();
      actionRemover = null;
    }

    this.registered = registered;
  }

  private void setActivated(boolean activated) {
    if (activated == this.activated) {
      return;
    }

    if (activated) {
      mouseMoveListenerRemover =
          editor.getBuffer().getMouseMoveListenerRegistrar().add(mouseMoveListener);
      mouseClickListenerRemover =
          editor.getBuffer().getMouseClickListenerRegistrar().add(mouseClickListener);
      nativeKeyUpListenerRemover =
          editor.getNativeKeyUpListenerRegistrar().add(keyUpListener);
      editor.addLineRenderer(referenceRenderer);
    } else {
      mouseMoveListenerRemover.remove();
      mouseMoveListenerRemover = null;
      mouseClickListenerRemover.remove();
      mouseClickListenerRemover = null;
      nativeKeyUpListenerRemover.remove();
      nativeKeyUpListenerRemover = null;
      referenceRenderer.resetReferences(editor.getRenderer(), lineFinder);
      editor.removeLineRenderer(referenceRenderer);
    }
    this.activated = activated;
  }

  private static boolean isGoToDefinitionSupported(PathUtil filePath) {
    String pathString = filePath.getPathString();
    return StringUtils.endsWithIgnoreCase(pathString, ".js")
        || StringUtils.endsWithIgnoreCase(pathString, ".html")
        || StringUtils.endsWithIgnoreCase(pathString, ".py");
  }

  private void goToDefinitionAtCurrentCaretPosition() {
    // TODO: Check here that our code model is fresh enough.
    // int caretOffset = getCaretOffset(editor.getWidget().getElement());
    SelectionModel selection = editor.getSelection();
    LineInfo lineInfo = new LineInfo(selection.getCursorLine(), selection.getCursorLineNumber());
    NavigableReference reference =
        referenceStore.findReference(lineInfo, selection.getCursorColumn(), true);
    navigateReference(reference);
  }

  private void navigateReference(NavigableReference reference) {
    if (reference == null) {
      return;
    }
    setActivated(false);
    reference.navigate(referenceNavigator);
  }

  private NavigableReference findReferenceAtMousePosition(int x, int y, boolean blocking) {
    int lineNumber = editor.getBuffer().convertYToLineNumber(y, true);
    LineInfo lineInfo = lineFinder.findLine(lineNumber);
    int column = editor.getBuffer().convertXToRoundedVisibleColumn(x, lineInfo.line());
    return referenceStore.findReference(lineInfo, column, blocking);
  }

  public void cleanup() {
    referenceStore.cleanup();
    if (anchorParser != null) {
      anchorParser.cleanup();
    }
    setActivated(false);
    setRegistered(false);
  }
}
