/*
 * Copyright 2000-2013 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.rename;

import com.intellij.CommonBundle;
import com.intellij.ide.TitledHandler;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.GeneratedSourcesFilter;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiDirectoryContainer;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.file.PsiPackageBase;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.refactoring.BaseRefactoringProcessor;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.util.Function;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * @author yole
 */
public abstract class DirectoryAsPackageRenameHandlerBase<T extends PsiDirectoryContainer> implements RenameHandler, TitledHandler {
  private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.rename.DirectoryAsPackageRenameHandler");

  protected abstract VirtualFile[] occursInPackagePrefixes(T aPackage);

  protected abstract boolean isIdentifier(String name, Project project);

  protected abstract String getQualifiedName(T aPackage);

  @Nullable
  protected abstract T getPackage(PsiDirectory psiDirectory);

  protected abstract BaseRefactoringProcessor createProcessor(final String newQName,
                                                              final Project project,
                                                              final PsiDirectory[] dirsToRename,
                                                              boolean searchInComments,
                                                              boolean searchInNonJavaFiles);

  @Override
  public boolean isAvailableOnDataContext(final DataContext dataContext) {
    PsiElement element = adjustForRename(dataContext, PsiElementRenameHandler.getElement(dataContext));
    if (element instanceof PsiDirectory) {
      final VirtualFile virtualFile = ((PsiDirectory)element).getVirtualFile();
      final Project project = element.getProject();
      if (Comparing.equal(project.getBaseDir(), virtualFile)) return false;
      if (ProjectRootManager.getInstance(project).getFileIndex().isInContent(virtualFile)) {
        return true;
      }
    }
    return false;
  }

  private PsiElement adjustForRename(DataContext dataContext, PsiElement element) {
    if (element instanceof PsiDirectoryContainer) {
      final Module module = LangDataKeys.MODULE.getData(dataContext);
      if (module != null) {
        PsiDirectory[] directories = ((PsiDirectoryContainer)element).getDirectories(GlobalSearchScope.moduleScope(module));
        Optional<PsiDirectory> directoryWithPackage = Arrays.stream(directories).filter(directory -> getPackage(directory) != null).findFirst();
        return directoryWithPackage.orElse(null);
      }
    }
    return element;
  }

  @Override
  public boolean isRenaming(final DataContext dataContext) {
    return isAvailableOnDataContext(dataContext);
  }

  @Override
  public void invoke(@NotNull final Project project, final Editor editor, final PsiFile file, final DataContext dataContext) {
    PsiElement element = adjustForRename(dataContext, PsiElementRenameHandler.getElement(dataContext));
    editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
    final PsiElement nameSuggestionContext = file.findElementAt(editor.getCaretModel().getOffset());
    doRename(element, project, nameSuggestionContext, editor);
  }

  @Override
  public void invoke(@NotNull final Project project, @NotNull final PsiElement[] elements, final DataContext dataContext) {
    PsiElement element = elements.length == 1 ? elements[0] : null;
    if (element == null) element = PsiElementRenameHandler.getElement(dataContext);
    final PsiElement nameSuggestionContext = element;
    element = adjustForRename(dataContext, element);
    LOG.assertTrue(element != null);
    Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
    doRename(element, project, nameSuggestionContext, editor);
  }

  private void doRename(PsiElement element, final Project project, PsiElement nameSuggestionContext, Editor editor) {
    final PsiDirectory psiDirectory = (PsiDirectory)element;
    final T aPackage = getPackage(psiDirectory);
    final String qualifiedName = aPackage != null ? getQualifiedName(aPackage) : "";
    if (aPackage == null || qualifiedName.length() == 0/*default package*/ ||
        !isIdentifier(psiDirectory.getName(), project)) {
      PsiElementRenameHandler.rename(element, project, nameSuggestionContext, editor);
    }
    else {
      PsiDirectory[] directories = aPackage.getDirectories();
      final VirtualFile[] virtualFiles = occursInPackagePrefixes(aPackage);
      if (virtualFiles.length == 0 && directories.length == 1) {
        PsiElementRenameHandler.rename(aPackage, project, nameSuggestionContext, editor);
      }
      else { // the directory corresponds to a package that has multiple associated directories
        final ProjectFileIndex projectFileIndex = ProjectRootManager.getInstance(project).getFileIndex();
        boolean inLib = false;
        for (PsiDirectory directory : directories) {
          inLib |= !projectFileIndex.isInContent(directory.getVirtualFile());
        }

        final PsiDirectory[] projectDirectories = aPackage.getDirectories(GlobalSearchScope.projectScope(project));
        if (inLib) {
          final Module module = ModuleUtilCore.findModuleForPsiElement(psiDirectory);
          LOG.assertTrue(module != null);
          PsiDirectory[] moduleDirs = null;
          if (nameSuggestionContext instanceof PsiPackageBase) {
            moduleDirs = aPackage.getDirectories(GlobalSearchScope.moduleScope(module));
            if (moduleDirs.length <= 1) {
              moduleDirs = null;
            }
          }
          final String promptMessage = "Package \'" +
                                       aPackage.getName() +
                                       "\' contains directories in libraries which cannot be renamed. Do you want to rename " +
                                       (moduleDirs == null ? "current directory" : "current module directories");
          if (projectDirectories.length > 0) {
            int ret = Messages
              .showYesNoCancelDialog(project, promptMessage + " or all directories in project?", RefactoringBundle.message("warning.title"),
                          RefactoringBundle.message("rename.current.directory"),
                            RefactoringBundle.message("rename.directories"), CommonBundle.getCancelButtonText(),
                          Messages.getWarningIcon());
            if (ret == Messages.CANCEL) return;
            renameDirs(project, nameSuggestionContext, editor, psiDirectory, aPackage,
                       ret == Messages.YES ? (moduleDirs == null ? new PsiDirectory[]{psiDirectory} : moduleDirs) : projectDirectories);
          }
          else {
            if (Messages.showOkCancelDialog(project, promptMessage + "?", RefactoringBundle.message("warning.title"),
                                    Messages.getWarningIcon()) == Messages.OK) {
              renameDirs(project, nameSuggestionContext, editor, psiDirectory, aPackage, psiDirectory);
            }
          }
        }
        else {
          final StringBuffer message = new StringBuffer();
          RenameUtil.buildPackagePrefixChangedMessage(virtualFiles, message, qualifiedName);
          buildMultipleDirectoriesInPackageMessage(message, getQualifiedName(aPackage), directories);
          message.append(RefactoringBundle.message("directories.and.all.references.to.package.will.be.renamed",
                                                   psiDirectory.getVirtualFile().getPresentableUrl()));
          int ret = Messages.showYesNoCancelDialog(project, message.toString(), RefactoringBundle.message("warning.title"),
                                        RefactoringBundle.message("rename.package.button.text"),
                                          RefactoringBundle.message("rename.directory.button.text"), CommonBundle.getCancelButtonText(),
                                        Messages.getWarningIcon());
          if (ret == Messages.YES) {
            PsiElementRenameHandler.rename(aPackage, project, nameSuggestionContext, editor);
          }
          else if (ret == Messages.NO) {
            renameDirs(project, nameSuggestionContext, editor, psiDirectory, aPackage, psiDirectory);
          }
        }
      }
    }
  }

  private void renameDirs(final Project project,
                          final PsiElement nameSuggestionContext,
                          final Editor editor,
                          final PsiDirectory contextDirectory,
                          final T aPackage,
                          final PsiDirectory... dirsToRename) {
    final RenameDialog dialog = new RenameDialog(project, contextDirectory, nameSuggestionContext, editor) {
      @Override
      protected void doAction() {
        String newQName = StringUtil.getQualifiedName(StringUtil.getPackageName(getQualifiedName(aPackage)), getNewName());
        BaseRefactoringProcessor moveProcessor = createProcessor(newQName, project, dirsToRename, isSearchInComments(),
                                                                 isSearchInNonJavaFiles());
        invokeRefactoring(moveProcessor);
      }
    };
    dialog.show();
  }

  public static void buildMultipleDirectoriesInPackageMessage(StringBuffer message,
                                                              String packageQname,
                                                              PsiDirectory[] directories) {
    message.append(RefactoringBundle.message("multiple.directories.correspond.to.package"));
    message.append(packageQname);
    message.append(":\n\n");
    final List<PsiDirectory> generated = new ArrayList<>();
    final List<PsiDirectory> source = new ArrayList<>();
    for (PsiDirectory directory : directories) {
      final VirtualFile virtualFile = directory.getVirtualFile();
      if (GeneratedSourcesFilter.isGeneratedSourceByAnyFilter(virtualFile, directory.getProject())) {
        generated.add(directory);
      } else {
        source.add(directory);
      }
    }
    final Function<PsiDirectory, String> directoryPresentation = directory -> directory.getVirtualFile().getPresentableUrl();
    message.append(StringUtil.join(source, directoryPresentation, "\n"));
    if (!generated.isEmpty()) {
      message.append("\n\nalso generated:\n");
      message.append(StringUtil.join(generated, directoryPresentation, "\n"));
      
    }
  }

  @Override
  public String getActionTitle() {
    return RefactoringBundle.message("rename.directory.title");
  }
}
