/*
 * Copyright 2003-2011 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 jetbrains.mps.plugin;

import com.intellij.CommonBundle;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataKeys;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.ui.InputValidator;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.ScrollPaneFactory;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import java.awt.*;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.*;
import java.util.List;
import java.util.regex.Pattern;

/**
 * fyodor, Aug 3, 2010
 */
public class CollectJUnitTestsFromPatternsAction extends AnAction {
  @Override
  public void update(AnActionEvent e) {
    super.update(e);
    e.getPresentation().setVisible(false);

    Editor editor = e.getData(DataKeys.EDITOR);
    if (editor == null) return;

    Project project = e.getData(DataKeys.PROJECT);
    if (project == null) return;

    int offset = editor.getCaretModel().getOffset();
    PsiFile file = PsiDocumentManager.getInstance(project).getCachedPsiFile(editor.getDocument());
    if (file == null) return;

    PsiElement element = file.findElementAt(offset);
    PsiClass pc = getPsiClass(element);
    if (pc != null) e.getPresentation().setVisible(true);
  }

  @Override
  public void actionPerformed(AnActionEvent e) {
    Editor editor = e.getData(DataKeys.EDITOR);
    if (editor == null) return;

    final Project project = e.getData(DataKeys.PROJECT);
    if (project == null) return;

    int offset = editor.getCaretModel().getOffset();
    PsiFile file = PsiDocumentManager.getInstance(project).getCachedPsiFile(editor.getDocument());
    if (file == null) return;

    PsiElement element = file.findElementAt(offset);
    final PsiClass pc = getPsiClass(element);
    if (pc == null) return;

    File baseDir = getProjectBaseDir(project);

    String ptns = showInputDialog(project, "I want cookie! Give me the cookie!", "Cookie monster");
    if (ptns == null) return;

    Map<File, Set<String>> includePathsMap = new HashMap<File, Set<String>>();
    Map<File, Set<String>> excludePathsMap = new HashMap<File, Set<String>>();

    final List<String> errors = new ArrayList<String>();

    for (String ptn : ptns.split("\\n")) {

      FilePattern fp;
      try {
        fp = FilePattern.fromString(ptn);
      } catch (FilePatternParseException ex) {
        errors.add("Bad cookie: \"" + ptn + "\"");
        continue;
      }

      ModulesSourcePath msp = new ModulesSourcePath(fp.modulePtn, project);

      PatternFileSearcher pfs = new PatternFileSearcher(fp.filePtn, Collections.unmodifiableList(msp.getSourcePath()));

      for (Pair<File, String> pear : pfs.getRelativePaths()) {
        Map<File, Set<String>> pathsMap = fp.include ? includePathsMap : excludePathsMap;
        Set<String> paths = pathsMap.get(pear.getFirst());
        if (paths == null) {
          paths = new HashSet<String>();
          pathsMap.put(pear.getFirst(), paths);
        }
        paths.add(pear.getSecond());
      }
    }

    final Set<String> suiteClasses = new TreeSet<String>();

    for (Map.Entry<File, Set<String>> en : includePathsMap.entrySet()) {
      Set<String> include = en.getValue();
      Set<String> exclude = excludePathsMap.get(en.getKey());
      if (exclude != null) {
        include.removeAll(exclude);
      }
      for (String p : include) {
        suiteClasses.add(filePathToJavaClass(p));
      }
    }

    final WriteAction<String> action = new WriteAction<String>() {
      @Override
      protected void run(Result<String> stringResult) throws Throwable {
        stringResult.setResult(null);

        StringBuilder sb = new StringBuilder("@SuiteClassSymbols({");
        String sep = "";
        for (String sc : suiteClasses) {
          sb.append(sep).append("\"").append(sc).append("\"");
          sep = ",\n";
        }
        sb.append("})");

        try {
          PsiJavaParserFacade javaParserFacade = JavaPsiFacade.getInstance(project).getParserFacade();
          PsiAnnotation newann = javaParserFacade.createAnnotationFromText(sb.toString(), pc.getParent());

          PsiAnnotation oldann = null;
          for (PsiAnnotation ann : pc.getModifierList().getAnnotations()) {
            if (String.valueOf(ann.getQualifiedName()).contains("SuiteClassSymbols")) {
              oldann = ann;
              break;
            }
          }
          if (oldann != null) {
            oldann.replace(newann);
          } else {
            pc.getParent().addBefore(newann, pc);
          }
        } catch (Exception ex) {
          stringResult.setResult(ex.toString());
        }
      }
    };

    if (!suiteClasses.isEmpty()) {
      CommandProcessor.getInstance().executeCommand(project, new Runnable() {
        public void run() {
          String error = action.execute().getResultObject();
          if (error != null) {
            errors.add(error);
          }
        }
      }, e.getPresentation().getText(), null);
    } else {
      errors.add("No cookie, no work. :-|");
    }

    if (!errors.isEmpty()) {
      StringBuilder sb = new StringBuilder("");
      String sep = "";
      for (String er : errors) {
        sb.append(sep).append(er);
        sep = "\n";
      }
      Messages.showErrorDialog(project, sb.toString(), "I have a bad feeling about this");
    }
  }

  private File getProjectBaseDir(Project prj) {
    if (prj != null) {
      VirtualFile bvf = prj.getBaseDir();
      if (bvf != null && bvf.isInLocalFileSystem()) {
        return new File(URI.create(bvf.getUrl()));
      }
    }
    return null;
  }

  private PsiClass getPsiClass(PsiElement element) {
    PsiClass pc = null;
    while (element != null) {
      pc = element instanceof PsiClass ? (PsiClass) element : null;
      if (pc != null) break;
      element = element.getParent();
    }
    return pc;
  }

  private String filePathToJavaClass(String filePath) {
    filePath = filePath.substring(0, filePath.lastIndexOf(".java"));
    return filePath.replaceAll(File.separator, ".");
  }

  public static String showInputDialog(Project project,
                                       @Nls String message,
                                       @Nls String title) {
    final Application application = ApplicationManager.getApplication();
    if (application.isUnitTestMode() || application.isHeadlessEnvironment()) {
      return null;
    } else {
      MyMessages.TextAreaInputDialog dialog = new MyMessages.TextAreaInputDialog(project, message, title, null, "", null, new String[]{CommonBundle.getOkButtonText(), CommonBundle.getCancelButtonText()}, 0);
      dialog.show();
      return dialog.getInputString();
    }
  }


  private static class FilePatternParseException extends Exception {
    public FilePatternParseException(String msg) {
      super(msg);
    }
  }

  private static class FilePattern {
    public final boolean include;
    public final String modulePtn;
    public final String filePtn;

    private FilePattern(boolean include, String modulePtn, String filePtn) {
      this.include = include;
      this.modulePtn = modulePtn;
      this.filePtn = filePtn;
    }

    public static FilePattern fromString(String ptn) throws FilePatternParseException {
      if (ptn.length() == 0) throw new FilePatternParseException("Empty pattern");

      boolean include = true;
      char sign = ptn.charAt(0);
      if (sign == '-' || sign == '+') {
        include = sign == '+';
        ptn = ptn.substring(1);
      }

      String modulePtn = "";
      int si = ptn.indexOf(":");
      if (si >= 0) {
        modulePtn = ptn.substring(0, si);
        ptn = ptn.substring(si + 1);
      }

      if (ptn.length() == 0) throw new FilePatternParseException("Empty file pattern");

      return new FilePattern(include, modulePtn, ptn);
    }

    public String toString() {
      return
          (include ? "+" : "-") +
              (modulePtn.length() == 0 ? "" : modulePtn + ":") +
              filePtn;
    }
  }

  private static class PatternFileSearcher {
    private String pattern;
    private Iterable<File> sourcePath;

    public PatternFileSearcher(String pattern, Iterable<File> sourcePath) {
      this.sourcePath = sourcePath;
      this.pattern = pattern;
    }

    public Iterable<Pair<File, String>> getRelativePaths() {
      Pattern ptn = Pattern.compile(FileUtil.convertAntToRegexp(pattern));
      List<Pair<File, String>> res = new ArrayList<Pair<File, String>>();

      for (File spd : sourcePath) {
        List<File> files = new ArrayList<File>();
        FileUtil.collectMatchedFiles(spd, ptn, files);
        for (File match : files) {
          res.add(new Pair<File, String>(spd, FileUtil.getRelativePath(spd, match)));
        }
      }
      return Collections.unmodifiableList(res);
    }

  }

  private static class ModulesSourcePath {
    private Iterable<Module> modules;

    public ModulesSourcePath(String namePattern, Project prj) {
      this.modules = getModules(namePattern, prj);
    }

    public List<File> getSourcePath() {
      List<File> res = new ArrayList<File>();
      for (Module module : modules) {
        for (VirtualFile svf : ModuleRootManager.getInstance(module).getSourceRoots()) {
          if (svf.isInLocalFileSystem()) {
            File fsf = new File(URI.create(svf.getUrl()));
            res.add(fsf);
          }
        }
      }
      return res;
    }

    private static Iterable<Module> getModules(String namePattern, Project prj) {
      List<Module> mdls = new ArrayList<Module>();
      if (namePattern == null || namePattern.length() == 0) {
        namePattern = ".*";
      } else {
        namePattern = namePattern.replaceAll("\\*", ".*");
      }
      Pattern ptn = Pattern.compile(namePattern);
      for (Module mdl : ModuleManager.getInstance(prj).getModules()) {
        if (ptn.matcher(mdl.getName()).matches()) {
          mdls.add(mdl);
        }
      }
      return Collections.unmodifiableList(mdls);
    }
  }

  private static class MyMessages extends Messages {

    private static class TextAreaInputDialog extends InputDialog {
      private JTextArea textArea;

      public TextAreaInputDialog(Project project,
                                 String message,
                                 String title,
                                 Icon icon,
                                 String initialValue,
                                 InputValidator validator,
                                 String[] options,
                                 int defaultOption) {
        super(project, message, title, icon, initialValue, validator, options, defaultOption);
      }

      protected JComponent createCenterPanel() {
        return null;
      }

      protected JComponent createNorthPanel() {
        JComponent panel = super.createNorthPanel();
        JComponent textField = null;
        JPanel messagePanel = null;
        try {
          textField = (JComponent) InputDialog.class.getMethod("getTextField").invoke(this);
        } catch (InvocationTargetException ignore) {
        } catch (NoSuchMethodException ignore) {
        } catch (IllegalAccessException ignore) {
        }

        if (textField != null) {
          messagePanel = (JPanel) textField.getParent();
          messagePanel.remove(textField); // xe-xe

          textArea = new JTextArea(10, 50);
          textArea.setWrapStyleWord(true);
          textArea.setLineWrap(true);
          textArea.getDocument().addDocumentListener(new DocumentAdapter() {
            protected void textChanged(final DocumentEvent e) {
            }
          });

          JComponent scrollPane = null;
          try {
            Method createScrollPane = ScrollPaneFactory.class.getMethod("createScrollPane", Component.class);
            scrollPane = (JScrollPane) createScrollPane.invoke(null, textArea);
          } catch (InvocationTargetException ignore) {
          } catch (NoSuchMethodException ignore) {
          } catch (IllegalAccessException ignore) {
          }

          if (scrollPane != null) {
            messagePanel.add(scrollPane, BorderLayout.SOUTH);
          }
        }

        return panel;
      }

      public JComponent getPreferredFocusedComponent() {
        return textArea;
      }

      @Nullable
      public String getInputString() {
        if (getExitCode() == 0) {
          return textArea.getText().trim();
        } else {
          return null;
        }
      }

    }
  }
}
