/**
 * 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 Jun 2, 2005
 * 
 * @author Fabio Zadrozny
 */
package org.python.pydev.plugin.nature;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.QualifiedName;
import org.python.pydev.core.ExtensionHelper;
import org.python.pydev.core.ICodeCompletionASTManager;
import org.python.pydev.core.IInterpreterInfo;
import org.python.pydev.core.IInterpreterManager;
import org.python.pydev.core.IModulesManager;
import org.python.pydev.core.IPythonNature;
import org.python.pydev.core.IPythonPathNature;
import org.python.pydev.core.MisconfigurationException;
import org.python.pydev.core.PropertiesHelper;
import org.python.pydev.core.PythonNatureWithoutProjectException;
import org.python.pydev.core.docutils.StringSubstitution;
import org.python.pydev.core.log.Log;
import org.python.pydev.plugin.PydevPlugin;
import org.python.pydev.shared_core.SharedCorePlugin;
import org.python.pydev.shared_core.io.FileUtils;
import org.python.pydev.shared_core.string.FastStringBuffer;
import org.python.pydev.shared_core.string.StringUtils;
import org.python.pydev.shared_core.structure.OrderedMap;

/**
 * @author Fabio Zadrozny
 */
public class PythonPathNature implements IPythonPathNature {

    private volatile IProject fProject;
    private volatile PythonNature fNature;

    /**
     * This is the property that has the python path - associated with the project.
     */
    private static QualifiedName projectSourcePathQualifiedName = null;

    static QualifiedName getProjectSourcePathQualifiedName() {
        if (projectSourcePathQualifiedName == null) {
            projectSourcePathQualifiedName = new QualifiedName(PydevPlugin.getPluginID(), "PROJECT_SOURCE_PATH");
        }
        return projectSourcePathQualifiedName;
    }

    /**
     * This is the property that has the external python path - associated with the project.
     */
    private static QualifiedName projectExternalSourcePathQualifiedName = null;

    static QualifiedName getProjectExternalSourcePathQualifiedName() {
        if (projectExternalSourcePathQualifiedName == null) {
            projectExternalSourcePathQualifiedName = new QualifiedName(PydevPlugin.getPluginID(),
                    "PROJECT_EXTERNAL_SOURCE_PATH");
        }
        return projectExternalSourcePathQualifiedName;
    }

    /**
     * This is the property that has the external python path - associated with the project.
     */
    private static QualifiedName projectVariableSubstitutionQualifiedName = null;

    static QualifiedName getProjectVariableSubstitutionQualifiedName() {
        if (projectVariableSubstitutionQualifiedName == null) {
            projectVariableSubstitutionQualifiedName = new QualifiedName(PydevPlugin.getPluginID(),
                    "PROJECT_VARIABLE_SUBSTITUTION");
        }
        return projectVariableSubstitutionQualifiedName;
    }

    @Override
    public void setProject(IProject project, IPythonNature nature) {
        this.fProject = project;
        this.fNature = (PythonNature) nature;
    }

    @Override
    public IPythonNature getNature() {
        return this.fNature;
    }

    private boolean waited = false;

    /**
     * Returns a list of paths with the complete pythonpath for this nature.
     * 
     * This includes the pythonpath for the project, all the referenced projects and the
     * system.
     */
    @Override
    public List<String> getCompleteProjectPythonPath(IInterpreterInfo interpreter, IInterpreterManager manager) {
        IModulesManager projectModulesManager = getProjectModulesManager();
        if (projectModulesManager == null) {
            if (!waited) {
                waited = true;
                for (int i = 0; i < 10 && projectModulesManager == null; i++) {
                    //We may get into a race condition, so, try to see if we can get it.
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        //OK
                    }
                    projectModulesManager = getProjectModulesManager();
                }
            }
        }
        if (projectModulesManager == null) {
            return null;
        }
        return projectModulesManager.getCompletePythonPath(interpreter, manager);
    }

    private IModulesManager getProjectModulesManager() {
        IPythonNature nature = fNature;
        if (nature == null) {
            return null;
        }

        ICodeCompletionASTManager astManager = nature.getAstManager();
        if (astManager == null) {
            // AST manager might not be yet available
            // Code completion job is scheduled to be run
            return null;
        }

        return astManager.getModulesManager();
    }

    /**
     * @return the project pythonpath with complete paths in the filesystem.
     */
    @Override
    public String getOnlyProjectPythonPathStr(boolean addExternal) throws CoreException {
        String source = null;
        String external = null;
        String contributed = null;
        IProject project = fProject;
        PythonNature nature = fNature;

        if (project == null || nature == null) {
            return "";
        }

        //Substitute with variables!
        StringSubstitution stringSubstitution = new StringSubstitution(nature);

        source = (String) getProjectSourcePath(true, stringSubstitution, RETURN_STRING_WITH_SEPARATOR);
        if (addExternal) {
            external = getProjectExternalSourcePath(true, stringSubstitution);
        }
        contributed = stringSubstitution.performPythonpathStringSubstitution(getContributedSourcePath(project));

        if (source == null) {
            source = "";
        }
        //we have to work on this one to resolve to full files, as what is stored is the position
        //relative to the project location
        List<String> strings = StringUtils.splitAndRemoveEmptyTrimmed(source, '|');
        FastStringBuffer buf = new FastStringBuffer();

        for (String currentPath : strings) {
            if (currentPath.trim().length() > 0) {
                IPath p = new Path(currentPath);

                if (SharedCorePlugin.inTestMode()) {
                    //in tests
                    buf.append(currentPath);
                    buf.append("|");
                    continue;
                }

                boolean found = false;
                p = p.removeFirstSegments(1); //The first segment should always be the project (historically it's this way, but having it relative would be nicer!?!).
                IResource r = project.findMember(p);
                if (r == null) {
                    r = project.getFolder(p);
                }
                if (r != null) {
                    IPath location = r.getLocation();
                    if (location != null) {
                        found = true;
                        buf.append(FileUtils.getFileAbsolutePath(location.toFile()));
                        buf.append("|");
                    }
                }
                if (!found) {
                    Log.log(IStatus.WARNING, "Unable to find the path " + currentPath + " in the project were it's \n"
                            + "added as a source folder for pydev (project: " + project.getName() + ") member:" + r,
                            null);
                }
            }
        }

        if (external == null) {
            external = "";
        }
        return buf.append("|").append(external).append("|").append(contributed).toString();
    }

    /**
     * Similar to the getOnlyProjectPythonPathStr method above but only for source files (not contributed nor external)
     * and return IResources (zip files or folders).
     */
    @Override
    public Set<IResource> getProjectSourcePathFolderSet() throws CoreException {
        String source = null;
        IProject project = fProject;
        PythonNature nature = fNature;

        Set<IResource> ret = new HashSet<>();
        if (project == null || nature == null) {
            return ret;
        }

        //Substitute with variables!
        StringSubstitution stringSubstitution = new StringSubstitution(nature);

        source = (String) getProjectSourcePath(true, stringSubstitution, RETURN_STRING_WITH_SEPARATOR);

        if (source == null) {
            return ret;
        }
        //we have to work on this one to resolve to full files, as what is stored is the position
        //relative to the project location
        List<String> strings = StringUtils.splitAndRemoveEmptyTrimmed(source, '|');

        for (String currentPath : strings) {
            if (currentPath.trim().length() > 0) {
                IPath p = new Path(currentPath);
                p = p.removeFirstSegments(1); //The first segment should always be the project (historically it's this way, but having it relative would be nicer!?!).
                IResource r = project.findMember(p);
                if (r == null) {
                    r = project.getFolder(p);
                }
                if (r != null && r.exists()) {
                    ret.add(r);
                }
            }
        }
        return ret;
    }

    /**
     * Gets the source path contributed by plugins.
     * 
     * See: http://sourceforge.net/tracker/index.php?func=detail&aid=1988084&group_id=85796&atid=577329
     * 
     * @throws CoreException
     */
    @SuppressWarnings("unchecked")
    private String getContributedSourcePath(IProject project) throws CoreException {
        FastStringBuffer buff = new FastStringBuffer();
        List<IPythonPathContributor> contributors = ExtensionHelper
                .getParticipants("org.python.pydev.pydev_pythonpath_contrib");
        for (IPythonPathContributor contributor : contributors) {
            String additionalPythonPath = contributor.getAdditionalPythonPath(project);
            if (additionalPythonPath != null && additionalPythonPath.trim().length() > 0) {
                if (buff.length() > 0) {
                    buff.append("|");
                }
                buff.append(additionalPythonPath.trim());
            }
        }
        return buff.toString();
    }

    @Override
    public void setProjectSourcePath(String newSourcePath) throws CoreException {
        PythonNature nature = fNature;

        if (nature != null) {
            nature.getStore().setPathProperty(PythonPathNature.getProjectSourcePathQualifiedName(), newSourcePath);
        }
    }

    @Override
    public void setProjectExternalSourcePath(String newExternalSourcePath) throws CoreException {
        PythonNature nature = fNature;
        if (nature != null) {
            nature.getStore().setPathProperty(PythonPathNature.getProjectExternalSourcePathQualifiedName(),
                    newExternalSourcePath);
        }
    }

    @Override
    public void setVariableSubstitution(Map<String, String> variableSubstitution) throws CoreException {
        PythonNature nature = fNature;
        if (nature != null) {
            nature.getStore().setMapProperty(PythonPathNature.getProjectVariableSubstitutionQualifiedName(),
                    variableSubstitution);
        }
    }

    @Override
    public void clearCaches() {
    }

    @Override
    public Set<String> getProjectSourcePathSet(boolean replace) throws CoreException {
        String projectSourcePath;
        PythonNature nature = fNature;
        if (nature == null) {
            return new HashSet<String>();
        }
        projectSourcePath = getProjectSourcePath(replace);
        return new HashSet<String>(StringUtils.splitAndRemoveEmptyTrimmed(projectSourcePath, '|'));
    }

    @Override
    public String getProjectSourcePath(boolean replace) throws CoreException {
        return (String) getProjectSourcePath(replace, null, RETURN_STRING_WITH_SEPARATOR);
    }

    @Override
    @SuppressWarnings("unchecked")
    public OrderedMap<String, String> getProjectSourcePathResolvedToUnresolvedMap() throws CoreException {
        return (OrderedMap<String, String>) getProjectSourcePath(true, null, RETURN_MAP_RESOLVED_TO_UNRESOLVED);
    }

    private static final int RETURN_STRING_WITH_SEPARATOR = 1;
    private static final int RETURN_MAP_RESOLVED_TO_UNRESOLVED = 2;

    /**
     * Function which can take care of getting the paths just for the project (i.e.: without external
     * source folders).
     * 
     * @param replace used only if returnType == RETURN_STRING_WITH_SEPARATOR.
     * 
     * @param substitution the object which will do the string substitutions (only internally used as an optimization as
     * creating the instance may be expensive, so, if some other place already creates it, it can be passed along).
     * 
     * @param returnType if RETURN_STRING_WITH_SEPARATOR returns a string using '|' as the separator. 
     * If RETURN_MAP_RESOLVED_TO_UNRESOLVED returns a map which points from the paths resolved to the maps unresolved.
     */
    private Object getProjectSourcePath(boolean replace, StringSubstitution substitution, int returnType)
            throws CoreException {
        String projectSourcePath;
        boolean restore = false;
        IProject project = fProject;
        PythonNature nature = fNature;

        if (project == null || nature == null) {
            if (returnType == RETURN_STRING_WITH_SEPARATOR) {
                return "";
            } else if (returnType == RETURN_MAP_RESOLVED_TO_UNRESOLVED) {
                return new OrderedMap<String, String>();
            } else {
                throw new AssertionError("Unexpected return: " + returnType);
            }
        }
        projectSourcePath = nature.getStore().getPathProperty(PythonPathNature.getProjectSourcePathQualifiedName());
        if (projectSourcePath == null) {
            //has not been set
            if (returnType == RETURN_STRING_WITH_SEPARATOR) {
                return "";
            } else if (returnType == RETURN_MAP_RESOLVED_TO_UNRESOLVED) {
                return new OrderedMap<String, String>();
            } else {
                throw new AssertionError("Unexpected return: " + returnType);
            }
        }

        if (replace && substitution == null) {
            substitution = new StringSubstitution(fNature);
        }

        //we have to validate it, because as we store the values relative to the workspace, and not to the 
        //project, the path may become invalid (in which case we have to make it compatible again).
        StringBuffer buffer = new StringBuffer();
        List<String> paths = StringUtils.splitAndRemoveEmptyTrimmed(projectSourcePath, '|');
        IPath projectPath = project.getFullPath();
        for (String path : paths) {
            if (path.trim().length() > 0) {
                if (path.indexOf("${") != -1) { //Account for the string substitution.
                    buffer.append(path);
                } else {
                    IPath p = new Path(path);
                    if (p.isEmpty()) {
                        continue; //go to the next...
                    }
                    if (projectPath != null && !projectPath.isPrefixOf(p)) {
                        p = p.removeFirstSegments(1);
                        p = projectPath.append(p);
                        restore = true;
                    }
                    buffer.append(p.toString());
                }
                buffer.append("|");
            }
        }

        //it was wrong and has just been fixed
        if (restore) {
            projectSourcePath = buffer.toString();
            setProjectSourcePath(projectSourcePath);
            if (nature != null) {
                //yeap, everything has to be done from scratch, as all the filesystem paths have just
                //been turned to dust!
                nature.rebuildPath();
            }
        }

        if (returnType == RETURN_STRING_WITH_SEPARATOR) {
            return trimAndReplaceVariablesIfNeeded(replace, projectSourcePath, nature, substitution);

        } else if (returnType == RETURN_MAP_RESOLVED_TO_UNRESOLVED) {
            String ret = StringUtils.leftAndRightTrim(projectSourcePath, '|');
            OrderedMap<String, String> map = new OrderedMap<String, String>();

            List<String> unresolvedVars = StringUtils.splitAndRemoveEmptyTrimmed(ret, '|');

            //Always resolves here!
            List<String> resolved = StringUtils.splitAndRemoveEmptyTrimmed(
                    substitution.performPythonpathStringSubstitution(ret), '|');

            int size = unresolvedVars.size();
            if (size != resolved.size()) {
                throw new AssertionError("Error: expected same size from:\n" + unresolvedVars + "\nand\n" + resolved);
            }
            for (int i = 0; i < size; i++) {
                String un = unresolvedVars.get(i);
                String res = resolved.get(i);
                map.put(res, un);
            }

            return map;
        } else {
            throw new AssertionError("Unexpected return: " + returnType);
        }

    }

    /**
     * Replaces the variables if needed.
     */
    private String trimAndReplaceVariablesIfNeeded(boolean replace, String projectSourcePath, PythonNature nature,
            StringSubstitution substitution)
            throws CoreException {
        String ret = StringUtils.leftAndRightTrim(projectSourcePath, '|');
        if (replace) {
            ret = substitution.performPythonpathStringSubstitution(ret);
        }
        return ret;
    }

    @Override
    public String getProjectExternalSourcePath(boolean replace) throws CoreException {
        return getProjectExternalSourcePath(replace, null);
    }

    private String getProjectExternalSourcePath(boolean replace, StringSubstitution substitution) throws CoreException {
        String extPath;

        PythonNature nature = fNature;
        if (nature == null) {
            return "";
        }

        //no need to validate because those are always 'file-system' related
        extPath = nature.getStore().getPathProperty(PythonPathNature.getProjectExternalSourcePathQualifiedName());

        if (extPath == null) {
            extPath = "";
        }

        if (replace && substitution == null) {
            substitution = new StringSubstitution(fNature);
        }
        return trimAndReplaceVariablesIfNeeded(replace, extPath, nature, substitution);
    }

    @Override
    public List<String> getProjectExternalSourcePathAsList(boolean replaceVariables) throws CoreException {
        String projectExternalSourcePath = getProjectExternalSourcePath(replaceVariables);
        List<String> externalPaths = StringUtils.splitAndRemoveEmptyTrimmed(projectExternalSourcePath, '|');
        return externalPaths;
    }

    @Override
    public Map<String, String> getVariableSubstitution() throws CoreException, MisconfigurationException,
            PythonNatureWithoutProjectException {
        return getVariableSubstitution(true);
    }

    /**
     * Returns the variables in the python nature and in the interpreter.
     */
    @Override
    public Map<String, String> getVariableSubstitution(boolean addInterpreterInfoSubstitutions) throws CoreException,
            MisconfigurationException, PythonNatureWithoutProjectException {
        PythonNature nature = this.fNature;
        if (nature == null) {
            return new HashMap<String, String>();
        }

        Map<String, String> variableSubstitution;
        if (addInterpreterInfoSubstitutions) {

            IInterpreterInfo info = nature.getProjectInterpreter();
            Properties stringSubstitutionVariables = info.getStringSubstitutionVariables();
            if (stringSubstitutionVariables == null) {
                variableSubstitution = new HashMap<String, String>();
            } else {
                variableSubstitution = PropertiesHelper.createMapFromProperties(stringSubstitutionVariables);
            }
        } else {
            variableSubstitution = new HashMap<String, String>();
        }

        //no need to validate because those are always 'file-system' related
        Map<String, String> variableSubstitution2 = nature.getStore().getMapProperty(
                PythonPathNature.getProjectVariableSubstitutionQualifiedName());
        if (variableSubstitution2 != null) {
            if (variableSubstitution != null) {
                variableSubstitution.putAll(variableSubstitution2);
            } else {
                variableSubstitution = variableSubstitution2;
            }
        }

        //never return null!
        if (variableSubstitution == null) {
            variableSubstitution = new HashMap<String, String>();
        }
        return variableSubstitution;
    }

}
