/*
 * Copyright 2000-2017 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 com.intellij.debugger.ui;

import com.intellij.codeInsight.hint.HintUtil;
import com.intellij.debugger.DebuggerBundle;
import com.intellij.debugger.DebuggerInvocationUtil;
import com.intellij.debugger.DebuggerManagerEx;
import com.intellij.debugger.actions.DebuggerActions;
import com.intellij.debugger.engine.DebugProcessImpl;
import com.intellij.debugger.engine.JVMName;
import com.intellij.debugger.engine.JVMNameUtil;
import com.intellij.debugger.engine.evaluation.*;
import com.intellij.debugger.engine.evaluation.expression.EvaluatorBuilderImpl;
import com.intellij.debugger.engine.evaluation.expression.ExpressionEvaluator;
import com.intellij.debugger.engine.events.DebuggerContextCommandImpl;
import com.intellij.debugger.impl.DebuggerContextImpl;
import com.intellij.debugger.impl.DebuggerSession;
import com.intellij.debugger.impl.DebuggerUtilsEx;
import com.intellij.debugger.impl.EditorTextProvider;
import com.intellij.debugger.ui.impl.DebuggerTreeRenderer;
import com.intellij.debugger.ui.impl.InspectDebuggerTree;
import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl;
import com.intellij.debugger.ui.impl.watch.WatchItemDescriptor;
import com.intellij.debugger.ui.tree.render.DescriptorLabelListener;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.CustomShortcutSet;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.psi.*;
import com.intellij.ui.SimpleColoredText;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.util.IncorrectOperationException;
import com.intellij.xdebugger.impl.evaluate.quick.common.AbstractValueHint;
import com.intellij.xdebugger.impl.evaluate.quick.common.ValueHintType;
import com.sun.jdi.Method;
import com.sun.jdi.PrimitiveValue;
import com.sun.jdi.Value;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;

/**
 * @author lex
 * @since Nov 24, 2003
 */
public class ValueHint extends AbstractValueHint {
  private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.ui.ValueHint");
  private PsiElement myCurrentExpression = null;
  private Value myValueToShow = null;

  private ValueHint(Project project, Editor editor, Point point, ValueHintType type, final PsiElement selectedExpression, final TextRange textRange) {
    super(project, editor, point, type, textRange);
    myCurrentExpression = selectedExpression;
  }

  public static ValueHint createValueHint(Project project, Editor editor, Point point, ValueHintType type) {
    Trinity<PsiElement, TextRange, Value> trinity = getSelectedExpression(project, editor, point, type);
    final ValueHint hint = new ValueHint(project, editor, point, type, trinity.getFirst(), trinity.getSecond());
    hint.myValueToShow = trinity.getThird();
    return hint;
  }

  @Override
  protected boolean canShowHint() {
    return myCurrentExpression != null;
  }


  @Nullable
  private ExpressionEvaluator getExpressionEvaluator(DebuggerContextImpl debuggerContext) throws EvaluateException {
    if (myCurrentExpression instanceof PsiExpression) {
      return EvaluatorBuilderImpl.getInstance().build(myCurrentExpression, debuggerContext.getSourcePosition());
    }

    TextWithImportsImpl textWithImports = new TextWithImportsImpl(CodeFragmentKind.EXPRESSION, myCurrentExpression.getText());
    CodeFragmentFactory factory = DebuggerUtilsEx.findAppropriateCodeFragmentFactory(textWithImports, myCurrentExpression);
    JavaCodeFragment codeFragment = factory.createCodeFragment(textWithImports, myCurrentExpression.getContext(), getProject());
    return factory.getEvaluatorBuilder().build(codeFragment, debuggerContext.getSourcePosition());
  }


  @Override
  protected void evaluateAndShowHint() {
    final DebuggerContextImpl debuggerContext = DebuggerManagerEx.getInstanceEx(getProject()).getContext();

    final DebuggerSession debuggerSession = debuggerContext.getDebuggerSession();
    if(debuggerSession == null || !debuggerSession.isPaused()) return;

    try {

      final ExpressionEvaluator evaluator = getExpressionEvaluator(debuggerContext);
      if (evaluator == null) return;

      debuggerContext.getDebugProcess().getManagerThread().schedule(new DebuggerContextCommandImpl(debuggerContext) {
        @Override
        public Priority getPriority() {
          return Priority.HIGH;
        }

        @Override
        public void threadAction() {
          try {
            final EvaluationContextImpl evaluationContext = debuggerContext.createEvaluationContext();

            final String expressionText = ReadAction.compute(() -> myCurrentExpression.getText());
            final TextWithImports text = new TextWithImportsImpl(CodeFragmentKind.EXPRESSION, expressionText);
            final Value value = myValueToShow != null? myValueToShow : evaluator.evaluate(evaluationContext);

            final WatchItemDescriptor descriptor = new WatchItemDescriptor(getProject(), text, value);
            if (!isActiveTooltipApplicable(value) || getType() == ValueHintType.MOUSE_OVER_HINT) {
              if (getType() == ValueHintType.MOUSE_OVER_HINT) {
                // force using default renderer for mouse over hint in order to not to call accidentally methods while rendering
                // otherwise, if the hint is invoked explicitly, show it with the right "auto" renderer
                descriptor.setRenderer(DebugProcessImpl.getDefaultRenderer(value));
              }
              descriptor.updateRepresentation(evaluationContext, new DescriptorLabelListener() {
                @Override
                public void labelChanged() {
                  if(getCurrentRange() != null) {
                    if(getType() != ValueHintType.MOUSE_OVER_HINT || descriptor.isValueValid()) {
                      final SimpleColoredText simpleColoredText = DebuggerTreeRenderer.getDescriptorText(debuggerContext, descriptor, true);
                      if (isActiveTooltipApplicable(value)){
                        simpleColoredText.append(" (" + DebuggerBundle.message("active.tooltip.suggestion") + ")", SimpleTextAttributes.GRAYED_ATTRIBUTES);
                      }
                      showHint(simpleColoredText, descriptor);
                    }
                  }
                }
              });
            }
            else {
              createAndShowTree(expressionText, descriptor);
            }
          }
          catch (EvaluateException e) {
            LOG.debug(e);
          }
        }

      });
    }
    catch (EvaluateException e) {
      LOG.debug(e);
    }
  }

  private void createAndShowTree(final String expressionText, final NodeDescriptorImpl descriptor) {
    final DebuggerTreeCreatorImpl creator = new DebuggerTreeCreatorImpl(getProject());
    DebuggerInvocationUtil.invokeLater(getProject(), () -> showTreePopup(creator, Pair.create(descriptor, expressionText)));
  }

  private static boolean isActiveTooltipApplicable(final Value value) {
    return value != null && !(value instanceof PrimitiveValue);
  }

  private void showHint(final SimpleColoredText text, final WatchItemDescriptor descriptor) {
    DebuggerInvocationUtil.invokeLater(getProject(), () -> {
      if(!isHintHidden()) {
        JComponent component;
        if (!isActiveTooltipApplicable(descriptor.getValue())) {
          component = HintUtil.createInformationLabel(text);
        }
        else {
          component = createExpandableHintComponent(text, () -> {
            final DebuggerContextImpl debuggerContext = DebuggerManagerEx.getInstanceEx(getProject()).getContext();
            final DebugProcessImpl debugProcess = debuggerContext.getDebugProcess();
            debugProcess.getManagerThread().schedule(new DebuggerContextCommandImpl(debuggerContext) {
                          @Override
                          public void threadAction() {
                            descriptor.setRenderer(debugProcess.getAutoRenderer(descriptor));
                            final String expressionText = ReadAction.compute(() -> myCurrentExpression.getText());
                            createAndShowTree(expressionText, descriptor);
                          }
                        });
          });
        }
        if (!showHint(component)) return;
        if(getType() == ValueHintType.MOUSE_CLICK_HINT) {
          HintUtil.createInformationLabel(text).requestFocusInWindow();
        }
      }
    });
  }

  public static InspectDebuggerTree createInspectTree(final NodeDescriptorImpl descriptor, Project project) {
    final InspectDebuggerTree tree = new InspectDebuggerTree(project);
    final AnAction setValueAction = ActionManager.getInstance().getAction(DebuggerActions.SET_VALUE);
    setValueAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0)), tree);
    Disposer.register(tree, new Disposable() {
      @Override
      public void dispose() {
        setValueAction.unregisterCustomShortcutSet(tree);
      }
    });
    tree.setInspectDescriptor(descriptor);
    DebuggerContextImpl context = DebuggerManagerEx.getInstanceEx(project).getContext();
    tree.rebuild(context);
    return tree;
  }

  @Nullable
  private static Pair<PsiElement, TextRange> findExpression(PsiElement element, boolean allowMethodCalls) {
    final EditorTextProvider textProvider = EditorTextProvider.EP.forLanguage(element.getLanguage());
    if (textProvider != null) {
      return textProvider.findExpression(element, allowMethodCalls);
    }
    return null;
  }

  private static Trinity<PsiElement, TextRange, Value> getSelectedExpression(final Project project, final Editor editor, final Point point, final ValueHintType type) {
    final Ref<PsiElement> selectedExpression = Ref.create(null);
    final Ref<TextRange> currentRange = Ref.create(null);
    final Ref<Value> preCalculatedValue = Ref.create(null);

    PsiDocumentManager.getInstance(project).commitAndRunReadAction(() -> {
      // Point -> offset
      final int offset = calculateOffset(editor, point);


      PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());

      if(psiFile == null || !psiFile.isValid()) {
        return;
      }

      int selectionStart = editor.getSelectionModel().getSelectionStart();
      int selectionEnd   = editor.getSelectionModel().getSelectionEnd();

      if((type == ValueHintType.MOUSE_CLICK_HINT || type == ValueHintType.MOUSE_ALT_OVER_HINT) && (selectionStart <= offset && offset <= selectionEnd)) {
        PsiElement ctx = (selectionStart > 0) ? psiFile.findElementAt(selectionStart - 1) : psiFile.findElementAt(selectionStart);
        try {
          String text = editor.getSelectionModel().getSelectedText();
          if(text != null && ctx != null) {
            final JVMElementFactory factory = JVMElementFactories.getFactory(ctx.getLanguage(), project);
            if (factory == null) {
              return;
            }
            selectedExpression.set(factory.createExpressionFromText(text, ctx));
            currentRange.set(new TextRange(editor.getSelectionModel().getSelectionStart(), editor.getSelectionModel().getSelectionEnd()));
          }
        }
        catch (IncorrectOperationException ignored) {
        }
      }

      if(currentRange.get() == null) {
        PsiElement elementAtCursor = psiFile.findElementAt(offset);
        if (elementAtCursor == null) {
          return;
        }
        Pair<PsiElement, TextRange> pair = findExpression(elementAtCursor, type == ValueHintType.MOUSE_CLICK_HINT || type == ValueHintType.MOUSE_ALT_OVER_HINT);
        if (pair == null) {
          if (type == ValueHintType.MOUSE_OVER_HINT) {
            final DebuggerSession debuggerSession = DebuggerManagerEx.getInstanceEx(project).getContext().getDebuggerSession();
            if(debuggerSession != null && debuggerSession.isPaused()) {
              final Pair<Method, Value> lastExecuted = debuggerSession.getProcess().getLastExecutedMethod();
              if (lastExecuted != null) {
                final Method method = lastExecuted.getFirst();
                if (method != null) {
                  final Pair<PsiElement, TextRange> expressionPair = findExpression(elementAtCursor, true);
                  if (expressionPair != null && expressionPair.getFirst() instanceof PsiMethodCallExpression) {
                    final PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression)expressionPair.getFirst();
                    final PsiMethod psiMethod = methodCallExpression.resolveMethod();
                    if (psiMethod != null) {
                      final JVMName jvmSignature = JVMNameUtil.getJVMSignature(psiMethod);
                      try {
                        if (method.name().equals(psiMethod.getName()) && method.signature().equals(jvmSignature.getName(debuggerSession.getProcess()))) {
                          pair = expressionPair;
                          preCalculatedValue.set(lastExecuted.getSecond());
                        }
                      }
                      catch (EvaluateException ignored) {
                      }
                    }
                  }
                }
              }
            }
          }
        }
        if (pair == null) {
          return;
        }
        selectedExpression.set(pair.getFirst());
        currentRange.set(pair.getSecond());
      }
    });
    return Trinity.create(selectedExpression.get(), currentRange.get(), preCalculatedValue.get());
  }
}
