/*
 * 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.execution.ExecutionException;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.*;
import com.intellij.execution.filters.RegexpFilter;
import com.intellij.execution.filters.TextConsoleBuilder;
import com.intellij.execution.filters.TextConsoleBuilderFactory;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.ide.highlighter.HtmlFileType;
import com.intellij.ide.highlighter.XmlFileType;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.fileTypes.FileTypes;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
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.SimpleJavaSdkType;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.*;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.xml.XmlDocument;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.SystemProperties;
import org.intellij.lang.xpath.xslt.XsltSupport;
import org.intellij.lang.xpath.xslt.associations.FileAssociationsManager;
import org.intellij.plugins.xpathView.XPathBundle;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public final class XsltRunConfiguration extends LocatableConfigurationBase implements ModuleRunConfiguration, RunConfigurationWithSuppressedDefaultDebugAction {
    private static final String NAME = "XSLT Configuration";

    private static final String STRICT_FILE_PATH_EXPR = "(file\\://?(?:/?\\p{Alpha}\\:)?(?:/\\p{Alpha}\\:)?[^:]+)";
    private static final String RELAXED_FILE_PATH_EXPR = "((?:file\\://?)?(?:/?\\p{Alpha}\\:)?(?:/\\p{Alpha}\\:)?[^:]+)";

    private static final String LOG_TAG = "(?:\\[[\\w ]+\\]\\:? +)?";

    public enum OutputType {
        CONSOLE, STDOUT
    }

    public enum JdkChoice {
        FROM_MODULE, JDK
    }

    private List<Pair<String, String>> myParameters = new ArrayList<>();
    private @Nullable VirtualFilePointer myXsltFile = null;
    private @Nullable VirtualFilePointer myXmlInputFile = null;
    private @NotNull OutputType myOutputType = OutputType.CONSOLE;
    private boolean mySaveToFile = false;
    private @NotNull JdkChoice myJdkChoice = JdkChoice.FROM_MODULE;
    private @Nullable FileType myFileType = XmlFileType.INSTANCE;

    public @NlsSafe String myOutputFile; // intentionally untracked. should it be?
    public boolean myOpenOutputFile;
    public boolean myOpenInBrowser;
    public boolean mySmartErrorHandling = true;
    @Deprecated // this is only used if the dynamic selection of a port fails
    public int myRunnerPort = 34873;
    public String myVmArguments;
    public @NlsSafe String myWorkingDirectory;
    public String myModule;
    public String myJdk;

    private @Nullable @NlsActions.ActionText String mySuggestedName;

    public XsltRunConfiguration(Project project, ConfigurationFactory factory) {
        super(project, factory, NAME);
        mySuggestedName = null;
    }

    @Override
    public @NotNull SettingsEditor<XsltRunConfiguration> getConfigurationEditor() {
        return new XsltRunSettingsEditor(getProject());
    }

    @Override
    public RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEnvironment executionEnvironment) throws ExecutionException {
        if (myXsltFile == null) throw new ExecutionException(XPathBundle.message("dialog.message.no.xslt.file.selected"));
        final VirtualFile baseFile = myXsltFile.getFile();

        final XsltCommandLineState state = new XsltCommandLineState(this, executionEnvironment);
        final TextConsoleBuilder builder = TextConsoleBuilderFactory.getInstance().createBuilder(getProject());
        builder.addFilter(new CustomRegexpFilter(getProject(), RegexpFilter.FILE_PATH_MACROS + "\\:" +
                "(?:(?: line )?" + RegexpFilter.LINE_MACROS + ")?" +
                "(?:\\:(?: column )?" + RegexpFilter.COLUMN_MACROS + ")?", baseFile, STRICT_FILE_PATH_EXPR));
        builder.addFilter(new CustomRegexpFilter(getProject(), LOG_TAG +
                RegexpFilter.FILE_PATH_MACROS + "\\:" +
                "(?:(?: line )?" + RegexpFilter.LINE_MACROS + ")?" +
                "(?:\\:(?: column )?" + RegexpFilter.COLUMN_MACROS + ")?", baseFile, RELAXED_FILE_PATH_EXPR));

        builder.addFilter(new CustomRegexpFilter(getProject(), RegexpFilter.FILE_PATH_MACROS + ";" +
                " \\w+ #" + RegexpFilter.LINE_MACROS +
                "(?:; \\w+ #" + RegexpFilter.COLUMN_MACROS + ")?", baseFile, STRICT_FILE_PATH_EXPR));
        builder.addFilter(new CustomRegexpFilter(getProject(), LOG_TAG +
                RegexpFilter.FILE_PATH_MACROS + ";" +
                " \\w+ #" + RegexpFilter.LINE_MACROS +
                "(?:; \\w+ #" + RegexpFilter.COLUMN_MACROS + ")?", baseFile, RELAXED_FILE_PATH_EXPR));

        builder.addFilter(new CustomRegexpFilter(getProject(), "(?:" + RegexpFilter.FILE_PATH_MACROS + ")?" +
                " line " + RegexpFilter.LINE_MACROS +
                "(?:\\:(?: column )?" + RegexpFilter.COLUMN_MACROS + ")?", baseFile, STRICT_FILE_PATH_EXPR));

        state.setConsoleBuilder(builder);
        return state;
    }

    @Override
    public void createAdditionalTabComponents(final AdditionalTabComponentManager manager, ProcessHandler startedProcess) {
      if (myOutputType == OutputType.CONSOLE) {
        final HighlightingOutputConsole console = new HighlightingOutputConsole(getProject(), myFileType);

          XsltCommandLineState state = startedProcess.getUserData(XsltCommandLineState.STATE);
          boolean debug = state != null && state.isDebugger();
          boolean consoleTabAdded = false;
        for (XsltRunnerExtension extension : XsltRunnerExtension.getExtensions(this, debug)) {
          if (extension.createTabs(getProject(), manager, console, startedProcess)) {
            consoleTabAdded = true;
          }
        }
        if (!consoleTabAdded) {
          manager.addAdditionalTabComponent(console, console.getTabTitle());    // TODO: verify parameter
        }

        final OutputTabAdapter listener = new OutputTabAdapter(startedProcess, console);
        if (startedProcess.isStartNotified()) {
          listener.startNotified(new ProcessEvent(startedProcess));
        }
        else {
          startedProcess.addProcessListener(listener);
        }
      }
    }

    @Override
    public RunConfiguration clone() {
        final XsltRunConfiguration configuration = (XsltRunConfiguration)super.clone();
        configuration.myParameters = new ArrayList<>(myParameters);
        if (myXsltFile != null) configuration.myXsltFile = VirtualFilePointerManager.getInstance().duplicate(myXsltFile, getProject(), null);
        if (myXmlInputFile != null) configuration.myXmlInputFile = VirtualFilePointerManager.getInstance().duplicate(myXmlInputFile, getProject(), null);
        return configuration;
    }

    @Override
    public void checkConfiguration() throws RuntimeConfigurationException {
        if (myXsltFile == null) {
            throw new RuntimeConfigurationError(XPathBundle.message("dialog.message.no.xslt.file.selected"));
        }
        if (myXsltFile.getFile() == null) {
            throw new RuntimeConfigurationError(XPathBundle.message("dialog.message.selected.xslt.file.not.found"));
        }
        if (myXmlInputFile == null) {
            throw new RuntimeConfigurationError(XPathBundle.message("dialog.message.no.xml.input.file.selected"));
        }
        if (myXmlInputFile.getFile() == null) {
            throw new RuntimeConfigurationError(XPathBundle.message("dialog.message.selected.xml.input.file.not.found"));
        }
        if (mySaveToFile) {
            if (isEmpty(myOutputFile)) {
                throw new RuntimeConfigurationError(XPathBundle.message("dialog.message.no.output.file.selected"));
            }
            final File f = new File(myOutputFile);
            if (f.isDirectory()) {
                throw new RuntimeConfigurationError(XPathBundle.message("dialog.message.selected.output.file.points.to.directory"));
            } else if (f.exists() && !f.canWrite()) {
                throw new RuntimeConfigurationError(XPathBundle.message("dialog.message.selected.output.file.not.writable"));
            }
        }
        if (getEffectiveJDK() == null) {
            throw new RuntimeConfigurationError(XPathBundle.message("dialog.message.no.jdk.available"));
        }
    }

    static boolean isEmpty(String file) {
        return file == null || file.isEmpty();
    }

    // return modules to compile before run. Null or empty list to build project
    @Override
    public Module @NotNull [] getModules() {
        return getModule() != null ? new Module[]{ getModule() } : Module.EMPTY_ARRAY;
    }

    @Override
    public void readExternal(@NotNull Element element) throws InvalidDataException {
        super.readExternal(element);
        DefaultJDOMExternalizer.readExternal(this, element);

        Element e = element.getChild("XsltFile");
        if (e != null) {
            final String url = e.getAttributeValue("url");
            if (url != null) {
                myXsltFile = VirtualFilePointerManager.getInstance().create(url, getProject(), null);
            }
        }
        e = element.getChild("XmlFile");
        if (e != null) {
            final String url = e.getAttributeValue("url");
            if (url != null) {
                myXmlInputFile = VirtualFilePointerManager.getInstance().create(url, getProject(), null);
            }
        }

        final Element parameters = element.getChild("parameters");
        if (parameters != null) {
            myParameters.clear();
            final List<Element> params = parameters.getChildren("param");
            for (Element p : params) {
                myParameters.add(Pair.create(p.getAttributeValue("name"), p.getAttributeValue("value")));
            }
        }

        final Element outputType = element.getChild("OutputType");
        if (outputType != null) {
            final String value = outputType.getAttributeValue("value");
            myOutputType = OutputType.valueOf(value);
            mySaveToFile = Boolean.parseBoolean(outputType.getAttributeValue("save-to-file"));
        }
        final Element fileType = element.getChild("FileType");
        if (fileType != null) {
            myFileType = getFileType(fileType.getAttributeValue("name"));
        }
        final Element jdkChoice = element.getChild("JdkChoice");
        if (jdkChoice != null) {
            myJdkChoice = JdkChoice.valueOf(jdkChoice.getAttributeValue("value"));
        }
    }

    private static @Nullable FileType getFileType(String value) {
        if (value == null) return null;
        return FileTypeManager.getInstance().findFileTypeByName(value);
    }

    @Override
    public void writeExternal(@NotNull Element element) throws WriteExternalException {
        super.writeExternal(element);
        DefaultJDOMExternalizer.writeExternal(this, element);

        Element e = new Element("XsltFile");
        if (myXsltFile != null) {
            e.setAttribute("url", myXsltFile.getUrl());
            element.addContent(e);
        }
        e = new Element("XmlFile");
        if (myXmlInputFile != null) {
            e.setAttribute("url", myXmlInputFile.getUrl());
            element.addContent(e);
        }

        final Element params = new Element("parameters");
        element.addContent(params);
        for (Pair<String, String> pair : myParameters) {
            final Element p = new Element("param");
            params.addContent(p);
            p.setAttribute("name", pair.getFirst());

            final String value = pair.getSecond();
            if (value != null) {
                p.setAttribute("value", value);
            }
        }

        final Element type = new Element("OutputType");
        type.setAttribute("value", myOutputType.name());
        type.setAttribute("save-to-file", String.valueOf(mySaveToFile));
        element.addContent(type);

        final Element fileType = new Element("FileType");
        if (myFileType != null) {
          fileType.setAttribute("name", myFileType.getName());
        }
        element.addContent(fileType);

        final Element choice = new Element("JdkChoice");
        choice.setAttribute("value", myJdkChoice.name());
        element.addContent(choice);
    }

    public List<Pair<String, String>> getParameters() {
        return myParameters;
    }

    public void setParameters(List<Pair<String, String>> params) {
        myParameters.clear();
        myParameters.addAll(params);
    }

    public String getVmArguments() {
        return myVmArguments;
    }

    public void setVmArguments(String vmArguments) {
        myVmArguments = vmArguments;
    }

    public void setXsltFile(@NotNull String xsltFile) {
        if (isEmpty(xsltFile)) {
            myXsltFile = null;
        } else {
            myXsltFile = VirtualFilePointerManager.getInstance().create(VfsUtilCore.pathToUrl(xsltFile).replace(File.separatorChar, '/'), getProject(), null);
        }
    }

    private void setXsltFile(VirtualFile virtualFile) {
        myXsltFile = VirtualFilePointerManager.getInstance().create(virtualFile, getProject(), null);
    }

    public @Nullable @NlsSafe String getXsltFile() {
        return myXsltFile != null ? myXsltFile.getPresentableUrl() : null;
    }

    public @Nullable VirtualFile findXsltFile() {
        return myXsltFile != null ? myXsltFile.getFile() : null;
    }

    public @Nullable VirtualFile findXmlInputFile() {
        return myXmlInputFile != null ? myXmlInputFile.getFile() : null;
    }

    public @Nullable @NlsSafe String getXmlInputFile() {
        return myXmlInputFile != null ? myXmlInputFile.getPresentableUrl() : null;
    }

    public @Nullable FileType getFileType() {
        return myFileType;
    }

    public void setFileType(@Nullable FileType fileType) {
        myFileType = fileType;
    }

    public @Nullable Module getModule() {
        return myModule != null ? ModuleManager.getInstance(getProject()).findModuleByName(myModule) : null;
    }

    public @Nullable Sdk getJdk() {
        return myJdk != null ? ProjectJdkTable.getInstance().findJdk(myJdk) : null;
    }

    public void setXmlInputFile(@NotNull String xmlInputFile) {
        if (isEmpty(xmlInputFile)) {
            myXmlInputFile = null;
        } else {
            myXmlInputFile = VirtualFilePointerManager.getInstance().create(VfsUtilCore.pathToUrl(xmlInputFile).replace(File.separatorChar, '/'), getProject(), null);
        }
    }

    public void setXmlInputFile(VirtualFile xmlInputFile) {
      myXmlInputFile = VirtualFilePointerManager.getInstance().create(xmlInputFile, getProject(), null);
    }

    public void setModule(Module module) {
        myModule = module != null ? module.getName() : null;
    }

    public void setJDK(Sdk projectJdk) {
        myJdk = projectJdk != null ? projectJdk.getName() : null;
    }

    public @NotNull OutputType getOutputType() {
        return myOutputType;
    }

    public void setOutputType(@NotNull OutputType outputType) {
        myOutputType = outputType;
    }

    public boolean isSaveToFile() {
        return mySaveToFile;
    }

    public void setSaveToFile(boolean saveToFile) {
        mySaveToFile = saveToFile;
    }

    public @NotNull JdkChoice getJdkChoice() {
        return myJdkChoice;
    }

    public void setJdkChoice(@NotNull JdkChoice jdkChoice) {
        myJdkChoice = jdkChoice;
    }

    private static Sdk ourDefaultSdk;

    private static synchronized Sdk getDefaultSdk() {
        if (ourDefaultSdk == null) {
            ourDefaultSdk = new SimpleJavaSdkType().createJdk("tmp", SystemProperties.getJavaHome());
        }

        return ourDefaultSdk;
    }

    public @Nullable Sdk getEffectiveJDK() {
        if (myJdkChoice == JdkChoice.JDK) {
            return myJdk != null ? ProjectJdkTable.getInstance().findJdk(myJdk) : null;
        }
        Sdk jdk = null;
        final Module module = getEffectiveModule();
        if (module != null) {
            jdk = ModuleRootManager.getInstance(module).getSdk();
        }
        if (jdk == null) {
            jdk = ProjectRootManager.getInstance(getProject()).getProjectSdk();
        }
        // EA-33419
        if (jdk == null || !(jdk.getSdkType() instanceof JavaSdkType)) {
          return getDefaultSdk();
        }
        return jdk;
    }

    public @Nullable Module getEffectiveModule() {
        //assert myJdkChoice == JdkChoice.FROM_MODULE;

        Module module = myJdkChoice == JdkChoice.FROM_MODULE ? getModule() : null;
        if (module == null && myXsltFile != null) {
            final VirtualFile file = myXsltFile.getFile();
            if (file != null) {
                final ProjectFileIndex index = ProjectRootManager.getInstance(getProject()).getFileIndex();
                module = index.getModuleForFile(file);
            }
        }
        return module;
    }

    @Override
    public String suggestedName() {
        return mySuggestedName;
    }

    public XsltRunConfiguration initFromFile(@NotNull XmlFile file) {
        assert XsltSupport.isXsltFile(file) : "Not an XSLT file: " + file.getName();
        mySuggestedName = file.getName();
        setName(mySuggestedName);

        final VirtualFile virtualFile = file.getVirtualFile();
        assert virtualFile != null : "No VirtualFile for " + file.getName();

        setXsltFile(virtualFile);

        final PsiFile[] associations = FileAssociationsManager.getInstance(file.getProject()).getAssociationsFor(file);
        if (associations.length > 0) {
            final VirtualFile assoc = associations[0].getVirtualFile();
            assert assoc != null;
            setXmlInputFile(assoc);
        }

        final XmlDocument document = file.getDocument();
        assert document != null : "XSLT file without document?";

        final XmlTag rootTag = document.getRootTag();
        assert rootTag != null : "XSLT file without root element?";

        final XmlTag[] params = rootTag.findSubTags("param", XsltSupport.XSLT_NS);
        for (XmlTag param : params) {
            final String name = param.getAttributeValue("name");
            if (name != null) {
                final Pair<String, String> pair = Pair.create(name, null);
                myParameters.add(pair);
            }
        }
        final XmlTag[] outputs = rootTag.findSubTags("output", XsltSupport.XSLT_NS);
        for (XmlTag output : outputs) {
            final String method = output.getAttributeValue("method");
            if ("xml".equals(method)) {
                setFileType(XmlFileType.INSTANCE);
            } else if ("html".equals(method)) {
                setFileType(HtmlFileType.INSTANCE);
            } else if ("text".equals(method)) {
                setFileType(FileTypes.PLAIN_TEXT);
            }
        }

        return this;
    }
}
