/*
 * 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.uiDesigner.snapShooter;

import com.intellij.execution.*;
import com.intellij.execution.application.ApplicationConfiguration;
import com.intellij.execution.application.ApplicationConfigurationType;
import com.intellij.execution.executors.DefaultRunExecutor;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ProgramRunner;
import com.intellij.execution.util.JreVersionDetector;
import com.intellij.icons.AllIcons;
import com.intellij.ide.IdeView;
import com.intellij.ide.highlighter.JavaHighlightingColors;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.uiDesigner.GuiFormFileType;
import com.intellij.uiDesigner.UIDesignerBundle;
import com.intellij.uiDesigner.designSurface.InsertComponentProcessor;
import com.intellij.uiDesigner.palette.ComponentItem;
import com.intellij.uiDesigner.palette.Palette;
import com.intellij.uiDesigner.radComponents.LayoutManagerRegistry;
import com.intellij.uiDesigner.radComponents.RadComponentFactory;
import com.intellij.uiDesigner.radComponents.RadContainer;
import com.intellij.util.IncorrectOperationException;
import icons.UIDesignerIcons;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

/**
 * @author yole
 */
public class CreateSnapShotAction extends AnAction {
  private static final Logger LOG = Logger.getInstance("com.intellij.uiDesigner.snapShooter.CreateSnapShotAction");

  @Override
  public void update(AnActionEvent e) {
    final Project project = e.getData(CommonDataKeys.PROJECT);
    final IdeView view = e.getData(LangDataKeys.IDE_VIEW);
    e.getPresentation().setVisible(project != null && view != null && hasDirectoryInPackage(project, view));
  }

  private static boolean hasDirectoryInPackage(final Project project, final IdeView view) {
    ProjectFileIndex projectFileIndex = ProjectRootManager.getInstance(project).getFileIndex();
    PsiDirectory[] dirs = view.getDirectories();
    for (PsiDirectory dir : dirs) {
      if (projectFileIndex.isInSourceContent(dir.getVirtualFile()) && JavaDirectoryService.getInstance().getPackage(dir) != null) {
        return true;
      }
    }
    return false;
  }

  public void actionPerformed(AnActionEvent e) {
    final Project project = e.getData(CommonDataKeys.PROJECT);
    final IdeView view = e.getData(LangDataKeys.IDE_VIEW);
    if (project == null || view == null) {
      return;
    }

    final PsiDirectory dir = view.getOrChooseDirectory();
    if (dir == null) return;

    final SnapShotClient client = new SnapShotClient();
    List<RunnerAndConfigurationSettings> appConfigurations = new ArrayList<RunnerAndConfigurationSettings>();
    RunnerAndConfigurationSettings snapshotConfiguration = null;
    boolean connected = false;

    ApplicationConfigurationType cfgType = ApplicationConfigurationType.getInstance();
    List<RunnerAndConfigurationSettings> racsi = RunManager.getInstance(project).getConfigurationSettingsList(cfgType);

    for(RunnerAndConfigurationSettings config: racsi) {
      if (config.getConfiguration() instanceof ApplicationConfiguration) {
        ApplicationConfiguration appConfig = (ApplicationConfiguration) config.getConfiguration();
        appConfigurations.add(config);
        if (appConfig.ENABLE_SWING_INSPECTOR) {
          SnapShooterConfigurationSettings settings = SnapShooterConfigurationSettings.get(appConfig);
          snapshotConfiguration = config;
          if (settings.getLastPort() > 0) {
            try {
              client.connect(settings.getLastPort());
              connected = true;
            }
            catch(IOException ex) {
              connected = false;
            }
          }
        }
        if (connected) break;
      }
    }

    if (snapshotConfiguration == null) {
      snapshotConfiguration = promptForSnapshotConfiguration(project, appConfigurations);
      if (snapshotConfiguration == null) return;
    }

    if (!connected) {
      int rc = Messages.showYesNoDialog(project, UIDesignerBundle.message("snapshot.run.prompt"),
                                        UIDesignerBundle.message("snapshot.title"), Messages.getQuestionIcon());
      if (rc == 1) return;
      final ApplicationConfiguration appConfig = (ApplicationConfiguration) snapshotConfiguration.getConfiguration();
      final SnapShooterConfigurationSettings settings = SnapShooterConfigurationSettings.get(appConfig);
      settings.setNotifyRunnable(new Runnable() {
        public void run() {
          SwingUtilities.invokeLater(new Runnable() {
            public void run() {
              Messages.showMessageDialog(project, UIDesignerBundle.message("snapshot.prepare.notice"),
                                         UIDesignerBundle.message("snapshot.title"), Messages.getInformationIcon());
              try {
                client.connect(settings.getLastPort());
              }
              catch(IOException ex) {
                Messages.showMessageDialog(project, UIDesignerBundle.message("snapshot.connection.error"),
                                           UIDesignerBundle.message("snapshot.title"), Messages.getErrorIcon());
                return;
              }
              runSnapShooterSession(client, project, dir, view);
            }
          });
        }
      });
               
      try {
        final ProgramRunner runner = RunnerRegistry.getInstance().getRunner(DefaultRunExecutor.EXECUTOR_ID, appConfig);
        LOG.assertTrue(runner != null, "Runner MUST not be null!");
        Executor executor = DefaultRunExecutor.getRunExecutorInstance();
        runner.execute(
          new ExecutionEnvironment(executor, runner, snapshotConfiguration, project));
      }
      catch (ExecutionException ex) {
        Messages.showMessageDialog(project, UIDesignerBundle.message("snapshot.run.error", ex.getMessage()),
                                   UIDesignerBundle.message("snapshot.title"), Messages.getErrorIcon());
      }
    }
    else {
      runSnapShooterSession(client, project, dir, view);
    }
  }

  private static void runSnapShooterSession(final SnapShotClient client, final Project project, final PsiDirectory dir, final IdeView view) {
    try {
      client.suspendSwing();
    }
    catch (IOException e1) {
      Messages.showMessageDialog(project, UIDesignerBundle.message("snapshot.connection.error"),
                                 UIDesignerBundle.message("snapshot.title"), Messages.getInformationIcon());
      return;
    }

    final MyDialog dlg = new MyDialog(project, client, dir);
    dlg.show();
    if (dlg.getExitCode() == DialogWrapper.OK_EXIT_CODE) {
      final int id = dlg.getSelectedComponentId();
      final Ref<Object> result = new Ref<Object>();
      ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
        public void run() {
          try {
            result.set(client.createSnapshot(id));
          }
          catch (Exception ex) {
            result.set(ex);
          }
        }
      }, UIDesignerBundle.message("progress.creating.snapshot"), false, project);

      String snapshot = null;
      if (result.get() instanceof String) {
        snapshot = (String) result.get();
      }
      else {
        Exception ex = (Exception) result.get();
        Messages.showMessageDialog(project, UIDesignerBundle.message("snapshot.create.error", ex.getMessage()),
                                   UIDesignerBundle.message("snapshot.title"), Messages.getErrorIcon());
      }

      if (snapshot != null) {
        final String snapshot1 = snapshot;
        ApplicationManager.getApplication().runWriteAction(new Runnable() {
          public void run() {
            CommandProcessor.getInstance().executeCommand(project, new Runnable() {
              public void run() {
                try {
                  PsiFile formFile = PsiFileFactory.getInstance(dir.getProject())
                    .createFileFromText(dlg.getFormName() + GuiFormFileType.DOT_DEFAULT_EXTENSION, snapshot1);
                  formFile = (PsiFile)dir.add(formFile);
                  formFile.getVirtualFile().setCharset(CharsetToolkit.UTF8_CHARSET);
                  formFile.getViewProvider().getDocument().setText(snapshot1);
                  view.selectElement(formFile);
                }
                catch (IncorrectOperationException ex) {
                  Messages.showMessageDialog(project, UIDesignerBundle.message("snapshot.save.error", ex.getMessage()),
                                             UIDesignerBundle.message("snapshot.title"), Messages.getErrorIcon());
                }
              }
            }, "", null);
          }
        });
      }
    }

    try {
      client.resumeSwing();
    }
    catch (IOException ex) {
      Messages.showErrorDialog(project, UIDesignerBundle.message("snapshot.connection.broken"),
                               UIDesignerBundle.message("snapshot.title"));
    }

    client.dispose();
  }

  @Nullable
  private static RunnerAndConfigurationSettings promptForSnapshotConfiguration(final Project project,
                                                                                   final List<RunnerAndConfigurationSettings> configurations) {
    if (configurations.isEmpty()) {
      Messages.showMessageDialog(project, UIDesignerBundle.message("snapshot.no.configuration.error"),
                                 UIDesignerBundle.message("snapshot.title"), Messages.getInformationIcon());
      return null;
    }

    for(int i=configurations.size()-1; i >= 0; i--) {
      final JreVersionDetector detector = new JreVersionDetector();
      final ApplicationConfiguration configuration = (ApplicationConfiguration)configurations.get(i).getConfiguration();
      if (!detector.isJre50Configured(configuration) && !detector.isModuleJre50Configured(configuration)) {
        configurations.remove(i);
      }
    }

    if (configurations.isEmpty()) {
      Messages.showMessageDialog(project, UIDesignerBundle.message("snapshot.no.compatible.configuration.error"),
                                 UIDesignerBundle.message("snapshot.title"), Messages.getInformationIcon());
      return null;
    }

    final RunnerAndConfigurationSettings snapshotConfiguration;
    if (configurations.size() == 1) {
      final int rc = Messages.showYesNoDialog(
        project,
        UIDesignerBundle.message("snapshot.confirm.configuration.prompt", configurations.get(0).getConfiguration().getName()),
        UIDesignerBundle.message("snapshot.title"),
        Messages.getQuestionIcon());
      if (rc == 1) {
        return null;
      }
      snapshotConfiguration = configurations.get(0);
    }
    else {
      String[] names = new String[configurations.size()];
      for(int i=0; i<configurations.size(); i++) {
        names [i] = configurations.get(i).getConfiguration().getName();
      }
      int rc = Messages.showChooseDialog(
        project,
        UIDesignerBundle.message("snapshot.choose.configuration.prompt"),
        UIDesignerBundle.message("snapshot.title"),
        Messages.getQuestionIcon(),
        names,
        names [0]
      );
      if (rc < 0) return null;
      snapshotConfiguration = configurations.get(rc);
    }
    ((ApplicationConfiguration) snapshotConfiguration.getConfiguration()).ENABLE_SWING_INSPECTOR = true;
    return snapshotConfiguration;
  }

  private static class MyDialog extends DialogWrapper {
    private JPanel myRootPanel;
    private JTree myComponentTree;
    private JTextField myFormNameTextField;
    private JLabel myErrorLabel;
    private final Project myProject;
    private final SnapShotClient myClient;
    private final PsiDirectory myDirectory;
    @NonNls private static final String SWING_PACKAGE = "javax.swing.";

    private MyDialog(Project project, final SnapShotClient client, final PsiDirectory dir) {
      super(project, true);
      myProject = project;
      myClient = client;
      myDirectory = dir;
      init();
      setTitle(UIDesignerBundle.message("snapshot.title"));
      final SnapShotTreeModel model = new SnapShotTreeModel(client);
      myComponentTree.setModel(model);
      myComponentTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
      myComponentTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
        public void valueChanged(TreeSelectionEvent e) {
          updateOKAction();
        }
      });
      for(int i=0; i<2; i++) {
        for(int row=myComponentTree.getRowCount()-1; row >= 0; row--) {
          myComponentTree.expandRow(row);
        }
      }
      myComponentTree.getSelectionModel().setSelectionPath(myComponentTree.getPathForRow(0));
      myFormNameTextField.setText(suggestFormName());

      final EditorColorsScheme globalScheme = EditorColorsManager.getInstance().getGlobalScheme();
      final TextAttributes attributes = globalScheme.getAttributes(JavaHighlightingColors.STRING);
      final SimpleTextAttributes titleAttributes =
        new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, attributes.getForegroundColor());

      myComponentTree.setCellRenderer(new ColoredTreeCellRenderer() {
        public void customizeCellRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
          SnapShotRemoteComponent rc = (SnapShotRemoteComponent) value;

          String className = rc.getClassName();
          if (className.startsWith(SWING_PACKAGE)) {
            append(className.substring(SWING_PACKAGE.length()), SimpleTextAttributes.REGULAR_ATTRIBUTES);
          }
          else {
            append(className, SimpleTextAttributes.REGULAR_ATTRIBUTES);
          }

          if (rc.getText().length() > 0) {
            append(" \"" + rc.getText() + "\"", titleAttributes);
          }
          if (rc.getLayoutManager().length() > 0) {
            append(" (" + rc.getLayoutManager() + ")", SimpleTextAttributes.GRAY_ATTRIBUTES);
          }

          if (rc.isTopLevel()) {
            setIcon(AllIcons.FileTypes.UiForm);
          }
          else {
            final Palette palette = Palette.getInstance(myProject);
            final ComponentItem item = palette.getItem(rc.getClassName());
            if (item != null) {
              setIcon(item.getSmallIcon());
            }
            else {
              setIcon(UIDesignerIcons.Unknown);
            }
          }
        }
      });
      myFormNameTextField.getDocument().addDocumentListener(new DocumentAdapter() {
        protected void textChanged(DocumentEvent e) {
          updateOKAction();
        }
      });
      updateOKAction();
    }

    @NonNls
    private String suggestFormName() {
      int count = 0;
      do {
        count++;
      }
      while(myDirectory.findFile("Form" + count + GuiFormFileType.DOT_DEFAULT_EXTENSION) != null);
      return "Form" + count;
    }

    private void updateOKAction() {
      final boolean selectedComponentValid = isSelectedComponentValid();
      setOKActionEnabled(isFormNameValid() && selectedComponentValid);
      if (myComponentTree.getSelectionPath() != null && !selectedComponentValid) {
        myErrorLabel.setText(UIDesignerBundle.message("snapshooter.invalid.container"));
      }
      else {
        myErrorLabel.setText(" ");
      }
    }

    private boolean isSelectedComponentValid() {
      final TreePath selectionPath = myComponentTree.getSelectionPath();
      if (selectionPath == null) return false;
      SnapShotRemoteComponent rc = (SnapShotRemoteComponent) selectionPath.getLastPathComponent();
      if (isValidComponent(rc)) return true;
      if (selectionPath.getPathCount() == 2) {
        // capture frame/dialog root pane when a frame or dialog itself is selected
        final SnapShotRemoteComponent[] children = rc.getChildren();
        return children != null && children.length > 0 && isValidComponent(children[0]);
      }
      return false;
    }

    private boolean isValidComponent(final SnapShotRemoteComponent rc) {
      PsiClass componentClass =
        JavaPsiFacade.getInstance(myProject).findClass(rc.getClassName().replace('$', '.'), GlobalSearchScope.allScope(myProject));
      while(componentClass != null) {
        if (JPanel.class.getName().equals(componentClass.getQualifiedName()) ||
            JTabbedPane.class.getName().equals(componentClass.getQualifiedName()) ||
            JScrollPane.class.getName().equals(componentClass.getQualifiedName()) ||
            JSplitPane.class.getName().equals(componentClass.getQualifiedName())) {
          return true;
        }
        componentClass = componentClass.getSuperClass();
      }

      return false;
    }

    private boolean isFormNameValid() {
      return myFormNameTextField.getText().length() > 0;
    }

    @Override @NonNls
    protected String getDimensionServiceKey() {
      return "CreateSnapShotAction.MyDialog";
    }

    @Override
    public JComponent getPreferredFocusedComponent() {
      return myFormNameTextField;
    }

    @NotNull
    @Override
    protected Action getOKAction() {
      final Action okAction = super.getOKAction();
      okAction.putValue(Action.NAME, UIDesignerBundle.message("create.snapshot.button"));
      return okAction;
    }

    @Override
    protected void doOKAction() {
      if (getOKAction().isEnabled()) {
        try {
          myDirectory.checkCreateFile(getFormName() + GuiFormFileType.DOT_DEFAULT_EXTENSION);
        }
        catch (IncorrectOperationException e) {
          JOptionPane.showMessageDialog(myRootPanel, UIDesignerBundle.message("error.form.already.exists", getFormName()));
          return;
        }
        if (!checkUnknownLayoutManagers(myDirectory.getProject())) return;
        close(OK_EXIT_CODE);
      }
    }

    private boolean checkUnknownLayoutManagers(final Project project) {
      final Set<String> layoutManagerClasses = new TreeSet<String>();
      final SnapShotRemoteComponent rc = (SnapShotRemoteComponent) myComponentTree.getSelectionPath().getLastPathComponent();
      assert rc != null;
      final Ref<Exception> err = new Ref<Exception>();
      Runnable runnable = new Runnable() {
        public void run() {
          try {
            collectUnknownLayoutManagerClasses(project, rc, layoutManagerClasses);
          }
          catch (IOException e) {
            err.set(e);
          }
        }
      };
      if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(runnable,
                                                                             UIDesignerBundle.message("progress.validating.layout.managers"),
                                                                             false, project)) {
        return false;
      }
      if (!err.isNull()) {
        Messages.showErrorDialog(myRootPanel, UIDesignerBundle.message("snapshot.connection.broken"), UIDesignerBundle.message("snapshot.title"));
        return false;
      }
      if (!layoutManagerClasses.isEmpty()) {
        StringBuilder builder = new StringBuilder(UIDesignerBundle.message("snapshot.unknown.layout.prefix"));
        for(String layoutManagerClass: layoutManagerClasses) {
          builder.append(layoutManagerClass).append("\n");
        }
        builder.append(UIDesignerBundle.message("snapshot.unknown.layout.prompt"));
        return Messages.showYesNoDialog(myProject, builder.toString(),
                                        UIDesignerBundle.message("snapshot.title"), Messages.getQuestionIcon()) == 0;
      }
      return true;
    }

    private void collectUnknownLayoutManagerClasses(final Project project, final SnapShotRemoteComponent rc,
                                                    final Set<String> layoutManagerClasses) throws IOException {
      RadComponentFactory factory = InsertComponentProcessor.getRadComponentFactory(project, rc.getClassName());
      if (factory instanceof RadContainer.Factory && rc.getLayoutManager().length() > 0 &&
          !LayoutManagerRegistry.isKnownLayoutClass(rc.getLayoutManager())) {
        layoutManagerClasses.add(rc.getLayoutManager());
      }

      SnapShotRemoteComponent[] children = rc.getChildren();
      if (children == null) {
        children = myClient.listChildren(rc.getId());
        rc.setChildren(children);
      }
      for(SnapShotRemoteComponent child: children) {
        collectUnknownLayoutManagerClasses(project, child, layoutManagerClasses);
      }
    }

    @Nullable
    protected JComponent createCenterPanel() {
      return myRootPanel;
    }

    public int getSelectedComponentId() {
      final TreePath selectionPath = myComponentTree.getSelectionPath();
      SnapShotRemoteComponent rc = (SnapShotRemoteComponent) selectionPath.getLastPathComponent();
      if (!isValidComponent(rc) && selectionPath.getPathCount() == 2) {
        // capture frame/dialog root pane when a frame or dialog itself is selected
        final SnapShotRemoteComponent[] children = rc.getChildren();
        if (children != null && children.length > 0 && isValidComponent(children [0])) {
          return children [0].getId();
        }
      }
      return rc.getId();
    }

    public String getFormName() {
      return myFormNameTextField.getText();
    }
  }
}
