/*
 * Copyright 2000-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 com.intellij.refactoring.move.moveInstanceMethod;

import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.SuggestedNameInfo;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.impl.source.jsp.jspJava.JspClass;
import com.intellij.psi.impl.source.resolve.graphInference.PsiPolyExpressionUtil;
import com.intellij.psi.search.searches.OverridingMethodsSearch;
import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.HelpID;
import com.intellij.refactoring.RefactoringActionHandler;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.makeStatic.MakeStaticHandler;
import com.intellij.refactoring.move.MoveInstanceMembersUtil;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

/**
 * @author ven
 */
public class MoveInstanceMethodHandler implements RefactoringActionHandler {
  private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.move.moveInstanceMethod.MoveInstanceMethodHandler");
  static final String REFACTORING_NAME = RefactoringBundle.message("move.instance.method.title");

  @Override
  public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext) {
    PsiElement element = CommonDataKeys.PSI_ELEMENT.getData(dataContext);
    editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
    if (element == null) {
      element = file.findElementAt(editor.getCaretModel().getOffset());
    }

    if (element == null) return;
    if (element instanceof PsiIdentifier) element = element.getParent();

    if (!(element instanceof PsiMethod)) {
      String message = RefactoringBundle.getCannotRefactorMessage(RefactoringBundle.message("error.wrong.caret.position.method"));
      CommonRefactoringUtil.showErrorHint(project, editor, message, REFACTORING_NAME, HelpID.MOVE_INSTANCE_METHOD);
      return;
    }
    if (LOG.isDebugEnabled()) {
      LOG.debug("Move Instance Method invoked");
    }
    invoke(project, new PsiElement[]{element}, dataContext);
  }

  @Override
  public void invoke(@NotNull final Project project, @NotNull final PsiElement[] elements, final DataContext dataContext) {
    if (elements.length != 1 || !(elements[0] instanceof PsiMethod)) return;
    final PsiMethod method = (PsiMethod)elements[0];
    String message = null;
    if (!method.getManager().isInProject(method)) {
      message = "Move method is not supported for non-project methods";
    } else if (method.isConstructor()) {
      message = RefactoringBundle.message("move.method.is.not.supported.for.constructors");
    } else if (method.getLanguage()!= JavaLanguage.INSTANCE) {
      message = RefactoringBundle.message("move.method.is.not.supported.for.0", method.getLanguage().getDisplayName());
    }
    else {
      final PsiClass containingClass = method.getContainingClass();
      if (containingClass != null && mentionTypeParameters(method)) {
        message = RefactoringBundle.message("move.method.is.not.supported.for.generic.classes");
      }
      else if (method.findSuperMethods().length > 0 ||
               OverridingMethodsSearch.search(method).toArray(PsiMethod.EMPTY_ARRAY).length > 0) {
        message = RefactoringBundle.message("move.method.is.not.supported.when.method.is.part.of.inheritance.hierarchy");
      }
      else {
        final Set<PsiClass> classes = MoveInstanceMembersUtil.getThisClassesToMembers(method).keySet();
        for (PsiClass aClass : classes) {
          if (aClass instanceof JspClass) {
            message = RefactoringBundle.message("synthetic.jsp.class.is.referenced.in.the.method");
            Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
            CommonRefactoringUtil.showErrorHint(project, editor, message, REFACTORING_NAME, HelpID.MOVE_INSTANCE_METHOD);
            break;
          }
        }
      }
    }
    if (message != null) {
      showErrorHint(project, dataContext, message);
      return;
    }

    final List<PsiVariable> suitableVariables = new ArrayList<>();
    message = collectSuitableVariables(method, suitableVariables);
    if (message != null) {
      final String unableToMakeStaticMessage = MakeStaticHandler.validateTarget(method);
      if (unableToMakeStaticMessage != null) {
        showErrorHint(project, dataContext, message);
      }
      else {
        final String suggestToMakeStaticMessage = "Would you like to make method \'" + method.getName() + "\' static and then move?";
        if (Messages
          .showYesNoCancelDialog(project, message + ". " + suggestToMakeStaticMessage,
                                 REFACTORING_NAME, Messages.getErrorIcon()) == Messages.YES) {
          MakeStaticHandler.invoke(method);
        }
      }
      return;
    }

    new MoveInstanceMethodDialog(
      method,
      suitableVariables.toArray(new PsiVariable[0])).show();
  }

  private static void showErrorHint(Project project, DataContext dataContext, String message) {
    Editor editor = dataContext == null ? null : CommonDataKeys.EDITOR.getData(dataContext);
    CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.getCannotRefactorMessage(message), REFACTORING_NAME, HelpID.MOVE_INSTANCE_METHOD);
  }

  @Nullable
  private static String collectSuitableVariables(final PsiMethod method, final List<PsiVariable> suitableVariables) {
    final List<PsiVariable> allVariables = new ArrayList<>();
    ContainerUtil.addAll(allVariables, method.getParameterList().getParameters());
    ContainerUtil.addAll(allVariables, method.getContainingClass().getFields());
    boolean classTypesFound = false;
    boolean resolvableClassesFound = false;
    boolean classesInProjectFound = false;
    for (PsiVariable variable : allVariables) {
      final PsiType type = variable.getType();
      if (type instanceof PsiClassType && !((PsiClassType)type).hasParameters()) {
        classTypesFound = true;
        final PsiClass psiClass = ((PsiClassType)type).resolve();
        if (psiClass != null && !(psiClass instanceof PsiTypeParameter)) {
          resolvableClassesFound = true;
          final boolean inProject = method.getManager().isInProject(psiClass);
          if (inProject) {
            classesInProjectFound = true;
            suitableVariables.add(variable);
          }
        }
      }
    }

    if (suitableVariables.isEmpty()) {
      if (!classTypesFound) {
        return RefactoringBundle.message("there.are.no.variables.that.have.reference.type");
      }
      else if (!resolvableClassesFound) {
        return RefactoringBundle.message("all.candidate.variables.have.unknown.types");
      }
      else if (!classesInProjectFound) {
        return RefactoringBundle.message("all.candidate.variables.have.types.not.in.project");
      }
    }
    return null;
  }

  public static String suggestParameterNameForThisClass(final PsiClass thisClass) {
    PsiManager manager = thisClass.getManager();
    PsiType type = JavaPsiFacade.getElementFactory(manager.getProject()).createType(thisClass);
    final SuggestedNameInfo suggestedNameInfo = JavaCodeStyleManager.getInstance(manager.getProject())
      .suggestVariableName(VariableKind.PARAMETER, null, null, type);
    return suggestedNameInfo.names.length > 0 ? suggestedNameInfo.names[0] : "";
  }

  public static Map<PsiClass, String> suggestParameterNames(final PsiMethod method, final PsiVariable targetVariable) {
    final Map<PsiClass, Set<PsiMember>> classesToMembers = MoveInstanceMembersUtil.getThisClassesToMembers(method);
    Map<PsiClass, String> result = new LinkedHashMap<>();
    for (Map.Entry<PsiClass, Set<PsiMember>> entry : classesToMembers.entrySet()) {
      PsiClass aClass = entry.getKey();
      final Set<PsiMember> members = entry.getValue();
      if (members.size() == 1 && members.contains(targetVariable)) continue;
      result.put(aClass, suggestParameterNameForThisClass(aClass));
    }
    return result;
  }

  private static boolean mentionTypeParameters(PsiMethod method) {
    PsiClass containingClass = method.getContainingClass();
    if (containingClass == null) return false;
    Set<PsiTypeParameter> typeParameters = ContainerUtil.newHashSet(PsiUtil.typeParametersIterable(containingClass));
    for (PsiParameter parameter : method.getParameterList().getParameters()) {
      if (PsiPolyExpressionUtil.mentionsTypeParameters(parameter.getType(), typeParameters)) return true;
    }
    return PsiPolyExpressionUtil.mentionsTypeParameters(method.getReturnType(), typeParameters);
  }
}
