/*
 * Copyright 2005 Sascha Weinreuter
 *
 * 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 org.intellij.lang.xpath.xslt.run;

import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
import com.intellij.openapi.fileTypes.*;
import com.intellij.openapi.fileTypes.impl.FileTypeRenderer;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleType;
import com.intellij.openapi.options.SettingsEditor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdkType;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkType;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.TextComponentAccessor;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.ui.*;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBRadioButton;
import com.intellij.ui.table.JBTable;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.PlatformColors;
import com.intellij.util.ui.UIUtil;
import org.intellij.lang.xpath.xslt.XsltSupport;
import org.intellij.lang.xpath.xslt.associations.FileAssociationsManager;
import org.intellij.lang.xpath.xslt.associations.impl.AnyXMLDescriptor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.File;
import java.util.*;
import java.util.List;

class XsltRunSettingsEditor extends SettingsEditor<XsltRunConfiguration> {
  static final boolean ALLOW_CHOOSING_SDK = !(StdFileTypes.JAVA instanceof PlainTextFileType);
  private final Project myProject;

  private Editor myEditor;

  static class Editor implements PanelWithAnchor {
    private JTabbedPane myComponent;

    private TextFieldWithBrowseButton myXsltFile;
    private ComboboxWithBrowseButton myXmlInputFile;
    private TextFieldWithBrowseButton myOutputFile;
    private JCheckBox myOpenOutputFile;
    private JCheckBox myOpenInBrowser;
    private JBTable myParameters;

    private ButtonGroup myOutputOptions;
    private JBRadioButton myShowInConsole;
    private JCheckBox mySaveToFile;
    private JRadioButton myShowInStdout;

    private RawCommandLineEditor myVmArguments;
    private JCheckBox mySmartErrorHandling;
    private TextFieldWithBrowseButton myWorkingDirectory;

    private ButtonGroup myJdkOptions;
    private JRadioButton myJdkChoice;
    private JRadioButton myModuleChoice;
    private ComboBox myModule;
    private ComboBox myJDK;
    private ComboBox myFileType;
    private JPanel myClasspathAndJDKPanel;
    private JPanel myPanelSettings;
    private JPanel myPanelAdvanced;
    private JBLabel myXSLTScriptFileLabel;
    private JPanel myParametersPanel;
    private JComponent anchor;

    private final AnyXMLDescriptor myXmlDescriptor;
    private final FileChooserDescriptor myXsltDescriptor;

    public Editor(final Project project) {
      final PsiManager psiManager = PsiManager.getInstance(project);

      myXsltDescriptor = new FileChooserDescriptor(true, false, false, false, false, false) {
        public boolean isFileVisible(final VirtualFile file, boolean showHiddenFiles) {
          if (file.isDirectory()) return true;
          if (!super.isFileVisible(file, showHiddenFiles)) return false;

          return ReadAction.compute(() -> {
            final PsiFile psiFile = psiManager.findFile(file);
            return psiFile != null && XsltSupport.isXsltFile(psiFile);
          });
        }
      };
      final TextComponentAccessor<JTextField> projectDefaultAccessor = new TextComponentAccessor<JTextField>() {
        public String getText(JTextField component) {
          final String text = component.getText();
          final VirtualFile baseDir = project.getBaseDir();
          return text.length() > 0 ? text : (baseDir != null ? baseDir.getPresentableUrl() : "");
        }

        public void setText(JTextField component, @NotNull String text) {
          component.setText(text);
        }
      };
      myXsltFile.addBrowseFolderListener("Choose XSLT File", null, project, myXsltDescriptor, projectDefaultAccessor);
      myXsltFile.getTextField().getDocument().addDocumentListener(new DocumentAdapter() {
        final VirtualFileManager fileMgr = VirtualFileManager.getInstance();
        final FileAssociationsManager associationsManager = FileAssociationsManager.getInstance(project);

        protected void textChanged(DocumentEvent e) {
          final String text = myXsltFile.getText();
          final JComboBox comboBox = myXmlInputFile.getComboBox();
          final Object oldXml = getXmlInputFile();
          if (text.length() != 0) {
            final ComboBoxModel model = comboBox.getModel();

            boolean found = false;
            for (int i = 0; i < model.getSize(); i++) {
              if (oldXml.equals(model.getElementAt(i))) found = true;
            }
            final VirtualFile virtualFile = fileMgr.findFileByUrl(VfsUtil.pathToUrl(text.replace(File.separatorChar, '/')));
            final PsiFile psiFile;
            if (virtualFile != null && (psiFile = psiManager.findFile(virtualFile)) != null) {
              final PsiFile[] files = associationsManager.getAssociationsFor(psiFile);

              final Object[] associations = new String[files.length];
              for (int i = 0; i < files.length; i++) {
                final VirtualFile f = files[i].getVirtualFile();
                assert f != null;
                associations[i] = f.getPath().replace('/', File.separatorChar);
              }
              comboBox.setModel(new DefaultComboBoxModel(associations));
            }
            if (!found) {
              comboBox.getEditor().setItem(oldXml);
            }
            comboBox.setSelectedItem(oldXml);
          }
          else {
            comboBox.setModel(new DefaultComboBoxModel(ArrayUtil.EMPTY_OBJECT_ARRAY));
            comboBox.getEditor().setItem(oldXml);
          }
        }
      });

      myXmlInputFile.getComboBox().setEditable(true);

      myXmlDescriptor = new AnyXMLDescriptor(false);
      myXmlInputFile.addBrowseFolderListener("Choose XML File", null, project, myXmlDescriptor, new TextComponentAccessor<JComboBox>() {
        public String getText(JComboBox comboBox) {
          Object item = comboBox.getEditor().getItem();
          if (item.toString().length() == 0) {
            final String text = projectDefaultAccessor.getText(myXsltFile.getChildComponent());
            final VirtualFile file =
              VirtualFileManager.getInstance().findFileByUrl(VfsUtil.pathToUrl(text.replace(File.separatorChar, '/')));
            if (file != null && !file.isDirectory()) {
              final VirtualFile parent = file.getParent();
              assert parent != null;
              return parent.getPresentableUrl();
            }
          }
          return item.toString();
        }

        public void setText(JComboBox comboBox, @NotNull String text) {
          comboBox.getEditor().setItem(text);
        }
      });

      myOutputFile.addBrowseFolderListener("Choose Output File", "The selected file will be overwritten during execution.",
                                           project, FileChooserDescriptorFactory.createSingleFileOrFolderDescriptor());

      final ItemListener outputStateListener = new ItemListener() {
        public void itemStateChanged(ItemEvent e) {
          updateOutputState();
        }
      };
      myShowInConsole.addItemListener(outputStateListener);
      myShowInStdout.addItemListener(outputStateListener);
      mySaveToFile.addItemListener(outputStateListener);
      myClasspathAndJDKPanel.setVisible(ALLOW_CHOOSING_SDK);
      updateOutputState();

      myFileType.setRenderer(new FileTypeRenderer() {
        @Override
        public void customize(JList list, FileType type, int index, boolean selected, boolean hasFocus) {
          if (type == null) {
            setIcon(AllIcons.Actions.Cancel);
            setText("Disabled");
          }
          else {
            super.customize(list, type, index, selected, hasFocus);
          }
        }
      });
      myFileType.setModel(new DefaultComboBoxModel(getFileTypes(project)));

      myParameters = new JBTable(new ParamTableModel());
      myParameters.setDefaultRenderer(String.class, new DefaultTableCellRenderer() {
        public Component getTableCellRendererComponent(JTable table,
                                                       Object value,
                                                       boolean isSelected,
                                                       boolean hasFocus,
                                                       int row,
                                                       int column) {
          setForeground(null);
          setToolTipText(null);
          if (column == 0) {
            if (table.getModel().getValueAt(row, 1) == null) {
              setForeground(PlatformColors.BLUE);
              setToolTipText("No value set for parameter '" + value + "'");
            }
          }
          else if (value == null) {
            value = "<none>";
          }
          return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        }
      });

      myParametersPanel.add(
        ToolbarDecorator.createDecorator(myParameters)
          .setAddAction(new AnActionButtonRunnable() {
            @Override
            public void run(AnActionButton button) {
              ((Editor.ParamTableModel)myParameters.getModel()).addParam();
            }
          }).setRemoveAction(new AnActionButtonRunnable() {
          @Override
          public void run(AnActionButton button) {
            ((Editor.ParamTableModel)myParameters.getModel()).removeParam(myParameters.getSelectedRow());
          }
        }).createPanel(), BorderLayout.CENTER);

      final Module[] modules = ModuleManager.getInstance(project).getModules();
      myModule.setModel(new DefaultComboBoxModel(ArrayUtil.mergeArrays(new Object[]{"<default>"}, modules)));
      myModule.setRenderer(new ListCellRendererWrapper() {
        @Override
        public void customize(JList list, Object value, int index, boolean selected, boolean hasFocus) {
          if (value instanceof Module) {
            final Module module = (Module)value;
            setText(ApplicationManager.getApplication().runReadAction(new Computable<String>() {
              public String compute() {
                return module.getName();
              }
            }));
            setIcon(ModuleType.get(module).getIcon());
          }
          else if (value instanceof String) {
            setText((String)value);
          }
        }
      });

      final List<Sdk> allJdks = ContainerUtil.filter(ProjectJdkTable.getInstance().getAllJdks(),
                                                     sdk -> sdk.getSdkType() instanceof JavaSdkType);
      myJDK.setModel(new DefaultComboBoxModel(allJdks.toArray()));
      if (allJdks.size() > 0) {
        myJDK.setSelectedIndex(0);
      }
      else {
        myJdkChoice.setEnabled(false);
        myJDK.setEnabled(false);
      }
      myJDK.setRenderer(new ListCellRendererWrapper<Sdk>() {
        @Override
        public void customize(JList list, final Sdk jdk, int index, boolean isSelected, boolean cellHasFocus) {
          if (jdk != null) {
            setText(ApplicationManager.getApplication().runReadAction(new Computable<String>() {
              public String compute() {
                return jdk.getName();
              }
            }));
            setIcon(((SdkType) jdk.getSdkType()).getIcon());
          }
        }
      });

      final ItemListener updateListener = new ItemListener() {
        public void itemStateChanged(ItemEvent e) {
          updateJdkState();
        }
      };
      myModuleChoice.addItemListener(updateListener);
      myJdkChoice.addItemListener(updateListener);
      updateJdkState();

      myWorkingDirectory
        .addBrowseFolderListener("Working Directory", null, project, FileChooserDescriptorFactory.createSingleFolderDescriptor());

      myVmArguments.setDialogCaption("VM Arguments");

      myPanelSettings.setBorder(new EmptyBorder(UIUtil.PANEL_SMALL_INSETS));
      myPanelAdvanced.setBorder(new EmptyBorder(UIUtil.PANEL_SMALL_INSETS));

      setAnchor(myShowInConsole);
    }

    @Override
    public JComponent getAnchor() {
      return anchor;
    }

    @Override
    public void setAnchor(@Nullable JComponent anchor) {
      this.anchor = anchor;
      myXSLTScriptFileLabel.setAnchor(anchor);
      myShowInConsole.setAnchor(anchor);
    }

    @SuppressWarnings({"UseOfObsoleteCollectionType"})
    private static Vector<FileType> getFileTypes(Project project) {
      final Vector<FileType> v = new Vector<>();

      final FileType[] fileTypes = FileTypeManager.getInstance().getRegisteredFileTypes();
      for (FileType fileType : fileTypes) {
        // get rid of file types useless for highlighting
        if (fileType == StdFileTypes.CLASS ||
            fileType == StdFileTypes.IDEA_PROJECT ||
            fileType == StdFileTypes.IDEA_WORKSPACE ||
            fileType == StdFileTypes.IDEA_MODULE ||
            fileType == StdFileTypes.GUI_DESIGNER_FORM) {
          continue;
        }

        try {
          if (fileType instanceof LanguageFileType) {
            final SyntaxHighlighter sh =
              SyntaxHighlighterFactory.getSyntaxHighlighter(((LanguageFileType)fileType).getLanguage(), project, null);
            v.add(fileType);
          }
        }
        catch (Throwable e) {
          Logger.getInstance(XsltRunSettingsEditor.class.getName()).info("Encountered incompatible FileType: " + fileType.getName(), e);
        }
      }
      Collections.sort(v, Comparator.comparing(FileType::getDescription));

      // off
      v.insertElementAt(null, 0);
      return v;
    }

    public void resetFrom(XsltRunConfiguration s) {
      myXsltFile.setText(s.getXsltFile());
      myXmlInputFile.getComboBox().setSelectedItem(s.getXmlInputFile());

      final VirtualFile xmlInputFile = s.findXmlInputFile();
      if (xmlInputFile != null) {
        final Module contextModule = ProjectRootManager.getInstance(s.getProject()).getFileIndex().getModuleForFile(xmlInputFile);
        if (contextModule != null) {
          myXmlDescriptor.putUserData(LangDataKeys.MODULE_CONTEXT, contextModule);
        }
        else {
          myXmlDescriptor.putUserData(LangDataKeys.MODULE_CONTEXT, s.getModule());
        }
      }
      else {
        myXmlDescriptor.putUserData(LangDataKeys.MODULE_CONTEXT, s.getModule());
      }

      myFileType.setSelectedItem(s.getFileType());
      myOutputFile.setText(s.myOutputFile);
      myOpenOutputFile.setSelected(s.myOpenOutputFile);
      myOpenInBrowser.setSelected(s.myOpenInBrowser);
      myParameters.setModel(new ParamTableModel(s.getParameters()));
      myVmArguments.setText(s.getVmArguments());
      myWorkingDirectory.setText(s.myWorkingDirectory);
      final Module module = s.getModule();
      if (module != null) {
        myModule.setSelectedItem(module);
        myXsltDescriptor.putUserData(LangDataKeys.MODULE_CONTEXT, module);
      }
      else {
        final VirtualFile xsltFile = s.findXsltFile();
        if (xsltFile != null) {
          myXsltDescriptor.putUserData(LangDataKeys.MODULE_CONTEXT,
                                       ProjectRootManager.getInstance(s.getProject()).getFileIndex().getModuleForFile(xsltFile));
        }
        myModule.setSelectedIndex(0);
      }
      myJDK.setSelectedItem(s.getJdk());
      mySmartErrorHandling.setSelected(s.mySmartErrorHandling);
      setSelectedIndex(myOutputOptions, s.getOutputType().ordinal());
      setSelectedIndex(myJdkOptions, s.getJdkChoice().ordinal());
      mySaveToFile.setSelected(s.isSaveToFile());
    }

    public void applyTo(XsltRunConfiguration s) {
      s.setXsltFile(myXsltFile.getText());
      s.setXmlInputFile(getXmlInputFile());
      s.setFileType((FileType)myFileType.getSelectedItem());
      s.myOutputFile = myOutputFile.getText();
      s.myOpenOutputFile = myOpenOutputFile.isSelected();
      s.myOpenInBrowser = myOpenInBrowser.isSelected();
      s.setParameters(((ParamTableModel)myParameters.getModel()).getParams());
      s.setVmArguments(myVmArguments.getText());
      s.myWorkingDirectory = myWorkingDirectory.getText();
      s.setModule(getModule());
      s.setJDK((Sdk)myJDK.getSelectedItem());
      s.setJdkChoice(XsltRunConfiguration.JdkChoice.values()[getSelectedIndex(myJdkOptions)]);
      s.mySmartErrorHandling = mySmartErrorHandling.isSelected();
      s.setOutputType(XsltRunConfiguration.OutputType.values()[getSelectedIndex(myOutputOptions)]);
      s.setSaveToFile(mySaveToFile.isSelected());
    }

    private void updateOutputState() {
      myOutputFile.setEnabled(mySaveToFile.isSelected());
      myOpenOutputFile.setEnabled(mySaveToFile.isEnabled() && mySaveToFile.isSelected());
      myOpenInBrowser.setEnabled(mySaveToFile.isEnabled() && mySaveToFile.isSelected());

      final int selectedIndex = getSelectedIndex(myOutputOptions);
      final boolean b = selectedIndex == XsltRunConfiguration.OutputType.CONSOLE.ordinal();
      myFileType.setEnabled(b);
    }

    private void updateJdkState() {
      myModule.setEnabled(getSelectedIndex(myJdkOptions) == XsltRunConfiguration.JdkChoice.FROM_MODULE.ordinal());
      myJDK.setEnabled(getSelectedIndex(myJdkOptions) == XsltRunConfiguration.JdkChoice.JDK.ordinal());
    }

    @Nullable
    private Module getModule() {
      final Object selectedItem = myModule.getSelectedItem();
      return selectedItem instanceof Module ? (Module)selectedItem : null;
    }

    private String getXmlInputFile() {
      final JComboBox comboBox = myXmlInputFile.getComboBox();
      final Object currentItem = comboBox.getEditor().getItem();
      String s = (String)(currentItem != null ? currentItem : comboBox.getSelectedItem());
      return s != null ? s : "";
    }

    private static void setSelectedIndex(ButtonGroup group, int i) {
      final Enumeration<AbstractButton> buttons = group.getElements();
      for (int j = 0; buttons.hasMoreElements(); j++) {
        group.setSelected(buttons.nextElement().getModel(), i == j);
      }
    }

    private static int getSelectedIndex(ButtonGroup group) {
      final ButtonModel selection = group.getSelection();
      if (selection == null) return -1;
      final Enumeration<AbstractButton> buttons = group.getElements();
      for (int i = 0; buttons.hasMoreElements(); i++) {
        final AbstractButton button = buttons.nextElement();
        if (group.isSelected(button.getModel())) return i;
      }
      return -1;
    }

    private static class ParamTableModel extends AbstractTableModel {
      static class Param {
        public String name;
        public String value;

        public Param(String name, String value) {
          this.name = name;
          this.value = value;
        }

        public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null || getClass() != o.getClass()) return false;

          final Param param = (Param)o;

          return name.equals(param.name) && value.equals(param.value);
        }

        public int hashCode() {
          int result = name.hashCode();
          result = 29 * result + value.hashCode();
          return result;
        }
      }

      private final List<Param> myParams = new ArrayList<>();

      public ParamTableModel() {
      }

      public ParamTableModel(List<Pair<String, String>> params) {
        for (Pair<String, String> pair : params) {
          myParams.add(new Param(pair.getFirst(), pair.getSecond()));
        }
      }

      public Class<?> getColumnClass(int columnIndex) {
        return String.class;
      }

      public int getRowCount() {
        return myParams.size();
      }

      public int getColumnCount() {
        return 2;
      }

      public String getColumnName(int column) {
        return column == 0 ? "Name" : "Value";
      }

      public boolean isCellEditable(int rowIndex, int columnIndex) {
        return true;
      }

      public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        // can happen if param is deleted while editing it
        if (rowIndex >= myParams.size()) return;

        final Param param = myParams.get(rowIndex);
        final String value = (String)aValue;
        if (columnIndex == 0) {
          param.name = value;
        }
        else {
          param.value = value;
        }
        fireTableCellUpdated(rowIndex, columnIndex);
      }

      public Object getValueAt(int rowIndex, int columnIndex) {
        final Param param = myParams.get(rowIndex);
        return columnIndex == 0 ? param.name : param.value;
      }

      public void addParam() {
        myParams.add(new Param("Param" + (myParams.size() + 1), null));
        fireTableRowsInserted(myParams.size() - 1, myParams.size() - 1);
      }

      public void removeParam(int selectedRow) {
        myParams.remove(selectedRow);
        fireTableRowsDeleted(selectedRow, selectedRow);
      }

      public List<Pair<String, String>> getParams() {
        final ArrayList<Pair<String, String>> pairs = new ArrayList<>(myParams.size());
        for (Param param : myParams) {
          pairs.add(Pair.create(param.name, param.value));
        }
        return pairs;
      }
    }

    public JComponent getComponent() {
      return myComponent;
    }
  }

  public XsltRunSettingsEditor(Project project) {
    myProject = project;
  }

  protected void resetEditorFrom(@NotNull XsltRunConfiguration s) {
    myEditor.resetFrom(s);
  }

  protected void applyEditorTo(@NotNull XsltRunConfiguration s) {
    myEditor.applyTo(s);
  }

  @NotNull
  protected JComponent createEditor() {
    myEditor = new Editor(myProject);
    return myEditor.getComponent();
  }

  protected void disposeEditor() {
    myEditor = null;
  }
}
