/**
 * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Eclipse Public License (EPL).
 * Please see the license.txt included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
/*
 * Created on August 20, 2013
 *
 * @author Andrew Ferrazzutti
 */

package org.python.pydev.ui.pythonpathconf;

import java.io.CharArrayWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.dialogs.ListDialog;
import org.python.pydev.core.ExtensionHelper;
import org.python.pydev.core.IInterpreterInfo;
import org.python.pydev.core.IInterpreterManager;
import org.python.pydev.core.log.Log;
import org.python.pydev.plugin.PydevPlugin;
import org.python.pydev.shared_core.structure.LinkedListWarningOnSlowOperations;
import org.python.pydev.shared_core.structure.Tuple;
import org.python.pydev.shared_core.structure.Tuple3;
import org.python.pydev.shared_ui.EditorUtils;
import org.python.pydev.shared_ui.UIConstants;
import org.python.pydev.shared_ui.utils.AsynchronousProgressMonitorWrapper;
import org.python.pydev.ui.dialogs.PyDialogHelpers;
import org.python.pydev.ui.pythonpathconf.IInterpreterProviderFactory.InterpreterType;

/**
 * This class uses code based from {@link AbstractInterpreterEditor} and
 * {@link AbstractInterpreterPreferencesPage} to form a somewhat lighter utility for auto-
 * configuring a PyDev interpreter (without having to rely on the Preferences dialog). Also
 * contains an interpreter auto-searching method {@link AutoConfigMaker#autoConfig(InterpreterType)}
 * implemented statically, for use by other dialogs (particularly {@link AbstractInterpreterEditor}.
 *
 * @author Andrew Ferrazzutti
 */
public class AutoConfigMaker {
    private InterpreterType interpreterType;
    private IInterpreterManager interpreterManager;
    private boolean advanced;

    private PrintWriter logger;

    private CharArrayWriter charWriter;
    private Map<String, IInterpreterInfo> nameToInfo;

    /**
     * Create a new AutoConfigMaker, which will hold all passed settings for automatically
     * creating a new interpreter configuration. Must call {@link AutoConfigMaker#autoConfigAttempt}
     * to actually create the configuration.
     * @param interpreterType The interpreter's Python type: Python, Jython, or IronPython.
     * @param advanced Set to true if advanced auto-config is to be used, which allows users to choose
     * an interpreter out of the ones found.
     * @param logger May be set to null to use a new logger.
     * @param nameToInfo A map of names as keys to the IInterpreterInfos of existing interpreters. Set
     * to null if no other interpreters exist at the time of the configuration attempt.
     */
    public AutoConfigMaker(InterpreterType interpreterType, boolean advanced,
            PrintWriter logger, Map<String, IInterpreterInfo> nameToInfo) {
        this.interpreterType = interpreterType;
        this.nameToInfo = nameToInfo;
        switch (interpreterType) {
            case JYTHON:
                interpreterManager = PydevPlugin.getJythonInterpreterManager(true);
                break;
            case IRONPYTHON:
                interpreterManager = PydevPlugin.getIronpythonInterpreterManager(true);
                break;
            default:
                interpreterManager = PydevPlugin.getPythonInterpreterManager(true);
        }
        this.advanced = advanced;

        if (logger != null) {
            this.charWriter = null;
            this.logger = logger;
        } else {
            //Use a new logger if one wasn't provided.
            this.charWriter = new CharArrayWriter();
            this.logger = new PrintWriter(this.charWriter);
        }
    }

    /**
     * Attempts to automatically find and apply an interpreter of the interpreter type specified
     * in the constructor, in cases when no interpreters of that type are yet configured.
     * @param onConfigComplete An optional JobChangeAdapter to be associated with the configure operation.
     */
    public boolean autoConfigSingleApply(JobChangeAdapter onConfigComplete) {
        if (interpreterManager.getInterpreterInfos().length != 0) {
            return false;
        }
        ObtainInterpreterInfoOperation operation = autoConfigSearch();
        //autoConfigSearch displays an error dialog if an interpreter couldn't be found, so don't display errors for null cases here.
        if (operation == null) {
            return false;
        }
        try {
            final IInterpreterInfo interpreterInfo = operation.result.makeCopy();
            final Set<String> interpreterNamesToRestore = new HashSet<String>(
                    Arrays.asList(operation.result.executableOrJar));

            //------------- Now, actually prepare the interpreter.
            Job applyOperationJob = new Job("Configure Interpreter") {

                @Override
                protected IStatus run(IProgressMonitor monitor) {
                    monitor = new AsynchronousProgressMonitorWrapper(monitor);
                    PyDialogHelpers.enableAskInterpreterStep(false);
                    monitor.beginTask("Restoring PYTHONPATH", IProgressMonitor.UNKNOWN);
                    try {
                        //set this interpreter as the only interpreter, since none existed before this one
                        interpreterManager.setInfos(new IInterpreterInfo[] { interpreterInfo },
                                interpreterNamesToRestore, monitor);
                    } catch (Exception e) {
                        Log.log(e);
                        //show the user a message (so that it does not fail silently)...
                        String errorMsg = "Error configuring the chosen interpreter.\n"
                                + "Make sure the file containing the interpreter did not get corrupted during the configuration process.";
                        ErrorDialog.openError(EditorUtils.getShell(), "Interpreter configuration failure",
                                errorMsg, PydevPlugin.makeStatus(IStatus.ERROR, "See error log for details.", e));
                        return Status.CANCEL_STATUS;
                    } finally {
                        monitor.done();
                        PyDialogHelpers.enableAskInterpreterStep(true);
                    }
                    return Status.OK_STATUS;
                }

            };

            applyOperationJob.setUser(true);
            if (onConfigComplete != null) {
                applyOperationJob.addJobChangeListener(onConfigComplete);
            }
            applyOperationJob.schedule();
            return true;

        } catch (Exception e) {
            Log.log(e);
            String errorMsg = "Error getting info on the interpreter selected by the auto-configurer.\n"
                    + "Try manual configuration instead.\n\n"
                    + "Common reasons include:\n\n" + "- Using an unsupported version\n"
                    + "  (Python and Jython require at least version 2.1 and IronPython 2.6).\n"
                    + "\n" + "- Specifying an invalid interpreter\n"
                    + "  (usually a link to the actual interpreter on Mac or Linux)";
            //show the user a message (so that it does not fail silently)...
            ErrorDialog.openError(EditorUtils.getShell(), "Unable to get info on the interpreter.",
                    errorMsg, PydevPlugin.makeStatus(IStatus.ERROR, "See error log for details.", e));
            return false;
        } finally {
            if (charWriter != null) {
                Log.logInfo(charWriter.toString());
            }
        }
    }

    /**
     * Searches for a valid interpreter.
     * If quick auto-config, returns the first non-failing interpreter found.
     * If advanced, allows the user to choose from a list of verified interpreters, and returns the chosen one.
     *
     * @return The interpreter found by quick auto-config, or the one chosen by the user for advanced auto-config.
     */
    public ObtainInterpreterInfoOperation autoConfigSearch() {
        // get the possible interpreters
        final List<PossibleInterpreter> possibleInterpreters = getPossibleInterpreters();
        // keep track of the selected item
        PossibleInterpreter selectedFromPossible = null;

        // Query them for validity. If advanced or none installed, nothing will be selected.
        // If using quick config, choose the first installed interpreter (possibly none).
        Tuple3<PossibleInterpreter, Boolean, List<Exception>> r = removeInvalidPossibles(possibleInterpreters);
        selectedFromPossible = r.o1;
        boolean foundDuplicate = r.o2;
        List<Exception> exceptions = r.o3;

        // If we don't have anything we can add, exit now with an error message
        if (possibleInterpreters.size() > 0) {
            if (selectedFromPossible == null) {
                if (advanced && possibleInterpreters.size() > 1) {
                    // if using advanced config & we have more than 1 to choose from, ask the user
                    selectedFromPossible = promptToSelectInterpreter(possibleInterpreters);
                    // if selectedFromPossible is still null, user must have cancelled
                    if (selectedFromPossible == null) {
                        return null;
                    }
                } else {
                    // else, we can just auto-select for them
                    selectedFromPossible = possibleInterpreters.get(0);
                }
            }
            return selectedFromPossible.getOperation();
        } else {
            showNothingToConfigureError(foundDuplicate, exceptions);
        }
        return null;
    }

    private class PossibleInterpreter {
        private IInterpreterProvider provider;
        private ObtainInterpreterInfoOperation quickOperation;
        private Tuple<String, String> interpreterNameAndExecutable;

        public PossibleInterpreter(IInterpreterProvider provider) {
            this.provider = provider;
        }

        /**
         * Indicates whether or not an interpreter is valid for use.
         * @return <code>false</code> if the interpreter is a duplicate of a configured one,
         * or <code>true</code> if it is valid for use, or is yet to be installed.
         * @throws Exception An exception is thrown if the interpreter cannot be configured at all.
         */
        public boolean isValid() throws Exception {
            if (needInstall()) {
                return true;
            }

            // Try a quick config of the provider.
            // If getNameAndExecutable is successful, the interpreter won't be null & will have a unique name,
            // but it may a duplicate of something already configured.
            if (InterpreterConfigHelpers.getDuplicatedMessageError(null, getNameAndExecutable().o2,
                    nameToInfo) != null) {
                return false;
            }
            this.quickOperation = createOperation(false, false);
            return true;
        }

        private ObtainInterpreterInfoOperation createOperation(boolean advanced, boolean showErrors) throws Exception {
            return InterpreterConfigHelpers.tryInterpreter(getNameAndExecutable(), interpreterManager, !advanced,
                    showErrors, logger, EditorUtils.getShell());
        }

        private Tuple<String, String> getNameAndExecutable() throws Exception {
            if (interpreterNameAndExecutable != null) {
                return interpreterNameAndExecutable;
            }
            String executable = provider.getExecutableOrJar();
            if (executable != null && executable.trim().length() > 0) {
                String name = provider.getName();
                if (name == null) {
                    name = executable;
                }
                if (nameToInfo != null) {
                    name = InterpreterConfigHelpers.getUniqueInterpreterName(name, nameToInfo);
                }
                interpreterNameAndExecutable = new Tuple<String, String>(name, executable);
            } else {
                throw new Exception("Provider is invalid because it returned null from getExecutableOrJar()");
            }
            return interpreterNameAndExecutable;
        }

        public ObtainInterpreterInfoOperation getOperation() {
            if (needInstall()) {
                SafeRunner.run(new SafeRunnable() {
                    @Override
                    public void run() throws Exception {
                        provider.runInstall();
                    }
                });
                if (needInstall()) {
                    // runInstall failed, nothing else we can do (SafeRunnable or
                    // the installer itself will have displayed an error)
                    return null;
                }
                // name & executable may have changed, so set it to null to mark it for update the next time it's needed
                interpreterNameAndExecutable = null;
            }

            if (!advanced && quickOperation != null) {
                return quickOperation;
            } else {
                // Re-run an operation if user has to select folders (if advanced),
                // if the provider was uninstalled (if quickOperation is null),
                // or if the interpreter was missing libs (advanced & null quickOperation).
                try {
                    return createOperation(advanced, true);
                } catch (Exception e) {
                    // Failed to create operation, as we did "showErrors=true" we don't
                    // need to display them again though, so simply log them and exit
                    Log.log(e);
                    return null;
                }
            }
        }

        public boolean needInstall() {
            return !provider.isInstalled();
        }

        public String getExecutableOrJar() {
            return provider.getExecutableOrJar();
        }
    }

    private PossibleInterpreter promptToSelectInterpreter(final List<PossibleInterpreter> possibleInterpreters) {
        // Now we need to prompt the user to choose.
        ListDialog listDialog = new ListDialog(EditorUtils.getShell());

        listDialog.setContentProvider(new ArrayContentProvider());
        listDialog.setLabelProvider(new LabelProvider() {
            @Override
            public Image getImage(Object element) {
                return PydevPlugin.getImageCache().get(UIConstants.PY_INTERPRETER_ICON);
            }

            @Override
            public String getText(Object element) {
                PossibleInterpreter possible = (PossibleInterpreter) element;
                return possible.getExecutableOrJar();
            }
        });
        listDialog.setInput(possibleInterpreters.toArray());
        listDialog.setMessage("Multiple possible interpreters are available.\n"
                + "Please select which one you want to install and configure.");

        if (listDialog.open() == ListDialog.OK) {
            Object[] result = listDialog.getResult();
            return (PossibleInterpreter) result[0];
        } else {
            return null;
        }
    }

    private Tuple3<PossibleInterpreter, Boolean, List<Exception>> removeInvalidPossibles(
            final List<PossibleInterpreter> possibleInterpreters) {

        boolean foundDuplicate = false;
        List<Exception> exceptions = new LinkedListWarningOnSlowOperations<Exception>();

        // Iterate through the interpreters, removing the invalid ones
        for (Iterator<PossibleInterpreter> iterator = possibleInterpreters.iterator(); iterator.hasNext();) {
            PossibleInterpreter possibleInterpreter = iterator.next();
            Boolean validStatus = null;
            // Calling isValid may be a lengthy operation
            try {
                validStatus = possibleInterpreter.isValid();
                if (!validStatus) {
                    foundDuplicate = true;
                    throw new Exception("Duplicate interpreter.");
                }
            } catch (Exception e) {
                // If validStatus is null, an exception was thrown by isValid; save the first error found.
                if (validStatus == null) {
                    exceptions.add(e);
                }
                // Remove the interpreter if it is invalid or a duplicate.
                // Exception to this rule: if using advanced config, allow interpreters with no lib folders.
                if (!advanced || !e.getMessage().startsWith(InterpreterConfigHelpers.ERMSG_NOLIBS)) {
                    iterator.remove();
                }
                continue;
            }

            // We want to early exit for quick config
            if (!advanced && !possibleInterpreter.needInstall()) {
                // Early exit
                return new Tuple3<>(possibleInterpreter, foundDuplicate, exceptions);
            }
        }

        // keep going
        return new Tuple3<>(null, foundDuplicate, exceptions);
    }

    private void showNothingToConfigureError(boolean foundDuplicate, List<Exception> exceptions) {
        String errorMsg = "Auto-configurer could not find a valid interpreter"
                + (foundDuplicate ? " that has not already been configured" : "") + ".\n"
                + "Please manually configure a new interpreter instead.";

        String typeSpecificMessage;
        switch (interpreterType) {
            case PYTHON:
                typeSpecificMessage = "\n\nNote: the system environment variables that are used "
                        + "when auto-searching for a Jython interpreter are the following:\n"
                        + "- PATH\n"
                        + "- PYTHONHOME / PYTHON_HOME";
                break;
            case JYTHON:
                typeSpecificMessage = "\n\nNote: the system environment variables that are used "
                        + "when auto-searching for a Jython interpreter are the following:\n"
                        + "- PATH\n"
                        + "- PYTHONHOME / PYTHON_HOME\n"
                        + "- JYTHONHOME / JYTHON_HOME";
                break;
            default:
                typeSpecificMessage = "";
        }

        String message;
        if (exceptions.size() > 0) {
            message = "Errors getting info on discovered interpreter(s).\n"
                    + "See error log for details.";
        } else if (foundDuplicate) {
            message = "All interpreters found are already being used.";
        } else {
            // If there are no duplicates nor an interpreter error, nothing was found at all.
            message = "No interpreters were found.\n"
                    + "Make sure an interpreter is in the system PATH.";
        }
        String dialogTitle = "Unable to auto-configure.";

        if (exceptions.size() > 0) {
            IStatus[] children = new IStatus[exceptions.size()];
            for (int i = 0; i < exceptions.size(); i++) {
                Exception exception = exceptions.get(i);
                children[i] = PydevPlugin.makeStatus(IStatus.ERROR, null, exception);
            }
            MultiStatus multiStatus = new MultiStatus(PydevPlugin.getPluginID(), IStatus.ERROR, children, message,
                    null);
            ErrorDialog.openError(EditorUtils.getShell(), dialogTitle, errorMsg + typeSpecificMessage, multiStatus);
        } else {
            Status status = PydevPlugin.makeStatus(IStatus.ERROR, message, null);
            ErrorDialog.openError(EditorUtils.getShell(), dialogTitle, errorMsg + typeSpecificMessage, status);
        }
    }

    private List<PossibleInterpreter> getPossibleInterpreters() {
        final List<IInterpreterProvider> providers = getAllProviders();
        final List<PossibleInterpreter> possibleInterpreters = new ArrayList<>(providers.size());
        for (IInterpreterProvider provider : providers) {
            PossibleInterpreter possibleInterpreter = new PossibleInterpreter(provider);
            possibleInterpreters.add(possibleInterpreter);
        }
        return possibleInterpreters;
    }

    private List<IInterpreterProvider> getAllProviders() {
        final List<IInterpreterProvider> providers = new ArrayList<IInterpreterProvider>();
        @SuppressWarnings("unchecked")
        List<IInterpreterProviderFactory> participants = ExtensionHelper
                .getParticipants(ExtensionHelper.PYDEV_INTERPRETER_PROVIDER);
        for (final IInterpreterProviderFactory providerFactory : participants) {
            SafeRunner.run(new SafeRunnable() {
                @Override
                public void run() throws Exception {
                    IInterpreterProvider[] ips = providerFactory.getInterpreterProviders(interpreterType);
                    if (ips != null) {
                        providers.addAll(Arrays.asList(ips));
                    }
                }
            });
        }
        return providers;
    }
}
