/**
 * 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.
 */
package org.python.pydev.editorinput;

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

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ElementListSelectionDialog;
import org.eclipse.ui.part.FileEditorInput;
import org.python.pydev.core.IPyStackFrame;
import org.python.pydev.core.IPythonPathNature;
import org.python.pydev.core.log.Log;
import org.python.pydev.editor.PyEdit;
import org.python.pydev.editor.codecompletion.revisited.PythonPathHelper;
import org.python.pydev.plugin.PydevPlugin;
import org.python.pydev.plugin.nature.PythonNature;
import org.python.pydev.shared_core.callbacks.ICallback;
import org.python.pydev.shared_core.io.FileUtils;
import org.python.pydev.shared_core.locator.GetContainers;
import org.python.pydev.shared_core.locator.GetFiles;
import org.python.pydev.shared_core.string.StringUtils;
import org.python.pydev.ui.filetypes.FileTypesPreferencesPage;

/**
 * Refactored from the PydevPlugin: helpers to find some IFile / IEditorInput
 * from a Path (or java.io.File)
 *
 * @author fabioz
 */
public class PySourceLocatorBase {

    /**
     * Helper class.
     */
    private abstract static class FindFromExistingEditors {
        private final Object matchName;

        protected FindFromExistingEditors(Object matchName) {
            this.matchName = matchName;
        }

        public IEditorInput findFromOpenedPyEdits() {
            Object ret = PyEdit.iterOpenEditorsUntilFirstReturn(new ICallback<Object, PyEdit>() {

                @Override
                public Object call(PyEdit pyEdit) {
                    IEditorInput editorInput = pyEdit.getEditorInput();
                    if (editorInput instanceof IPathEditorInput) {
                        IPathEditorInput pathEditorInput = (IPathEditorInput) editorInput;
                        IPath localPath = pathEditorInput.getPath();
                        if (localPath != null) {
                            if (matchesPath(matchName, editorInput, localPath)) {
                                return editorInput;
                            }
                        }
                    } else {
                        File editorFile = pyEdit.getEditorFile();
                        if (editorFile != null) {
                            if (matchesFile(matchName, editorInput, editorFile)) {
                                return editorInput;
                            }
                        }
                    }
                    return null;
                }
            });
            return (IEditorInput) ret;
        }

        protected abstract boolean matchesFile(final Object match, IEditorInput editorInput, File editorFile);

        protected abstract boolean matchesPath(final Object match, IEditorInput editorInput, IPath localPath);

    }

    private static class FindFromExistingEditorsName extends PySourceLocatorBase.FindFromExistingEditors {
        protected FindFromExistingEditorsName(String matchName) {
            super(matchName);
        }

        @Override
        protected boolean matchesFile(final Object match, IEditorInput editorInput,
                File editorFile) {
            String matchName = (String) match;
            if (editorFile.getName().equals(matchName)) {
                return true;
            }
            return false;
        }

        @Override
        protected boolean matchesPath(final Object match, IEditorInput editorInput, IPath localPath) {
            String matchName = (String) match;
            String considerName = localPath.segment(localPath.segmentCount() - 1);
            if (matchName.equals(considerName)) {
                return true;
            }
            return false;
        }

    }

    private static class FindFromExistingEditorsFile extends PySourceLocatorBase.FindFromExistingEditors {
        protected FindFromExistingEditorsFile(File matchFile) {
            super(matchFile);
        }

        @Override
        protected boolean matchesFile(final Object match, IEditorInput editorInput,
                File editorFile) {
            File matchFile = (File) match;
            if (editorFile.equals(matchFile)) {
                return true;
            }
            return false;
        }

        @Override
        protected boolean matchesPath(final Object match, IEditorInput editorInput, IPath localPath) {
            File matchFile = (File) match;
            if (Path.fromOSString(FileUtils.getFileAbsolutePath(matchFile)).equals(localPath)) {
                return true;
            }
            return false;
        }

    }

    private final static GetFiles getFiles = new GetFiles() {

        @Override
        protected IFile getFileInProject(IPath location, IProject project) {
            IFile file = super.getFileInProject(location, project);
            if (file != null) {
                return file;
            }
            PythonNature nature = PythonNature.getPythonNature(project);
            if (nature != null) {
                IPythonPathNature pythonPathNature = nature.getPythonPathNature();
                try {
                    //Paths
                    Set<IResource> projectSourcePathSet = pythonPathNature.getProjectSourcePathFolderSet();
                    for (IResource iResource : projectSourcePathSet) {
                        if (iResource instanceof IContainer) {
                            //I.e.: don't consider zip files
                            IContainer iContainer = (IContainer) iResource;
                            file = getFileInContainer(location, iContainer);
                            if (file != null) {
                                return file;
                            }
                        }
                    }
                } catch (CoreException e) {
                    Log.log(e);
                }
            }
            return null;
        };
    };

    private final static GetContainers getContainers = new GetContainers() {

        @Override
        protected IContainer getContainerInProject(IPath location, IProject project) {
            IContainer file = super.getContainerInProject(location, project);
            if (file != null) {
                return file;
            }
            PythonNature nature = PythonNature.getPythonNature(project);
            if (nature != null) {
                IPythonPathNature pythonPathNature = nature.getPythonPathNature();
                try {
                    //Paths
                    Set<IResource> projectSourcePathSet = pythonPathNature.getProjectSourcePathFolderSet();
                    for (IResource iResource : projectSourcePathSet) {
                        if (iResource instanceof IContainer) {
                            //I.e.: don't consider zip files
                            IContainer iContainer = (IContainer) iResource;
                            file = getContainerInContainer(location, iContainer);
                            if (file != null) {
                                return file;
                            }
                        }
                    }
                } catch (CoreException e) {
                    Log.log(e);
                }
            }
            return null;
        };
    };

    /**
     * This method will try to find the most likely file that matches the given path,
     * considering:
     * - The workspace files
     * - The open editors
     *
     * and if all fails, it'll still ask the user which path should be used.
     *
     *
     * @param path
     * @return
     */
    public IEditorInput createEditorInput(IPath path, IProject project) {
        return createEditorInput(path, true, null, project);
    }

    public IEditorInput createEditorInput(IPath path) {
        return createEditorInput(path, true, null, null);
    }

    public IFile[] getFilesForLocation(IPath location, IProject project, boolean stopOnFirst) {
        return getFiles.getFilesForLocation(location, project, stopOnFirst);

    }

    public IFile getFileForLocation(IPath location, IProject project) {
        return getFiles.getFileForLocation(location, project);
    }

    /**
     * @param file the file we want to get in the workspace
     * @return a workspace file that matches the given file.
     */
    public IFile getWorkspaceFile(File file, IProject project) {
        return getFileForLocation(Path.fromOSString(file.getAbsolutePath()), project);
    }

    /**
     * @param file the file we want to get in the workspace
     * @return a workspace file that matches the given file.
     */
    public IFile[] getWorkspaceFiles(File file) {
        boolean stopOnFirst = false;
        IFile[] files = getFilesForLocation(Path.fromOSString(file.getAbsolutePath()), null, stopOnFirst);
        if (files == null || files.length == 0) {
            return null;
        }

        return files;
    }

    public IContainer getContainerForLocation(IPath location, IProject project) {
        return getContainers.getContainerForLocation(location, project);
    }

    public IContainer[] getContainersForLocation(IPath location) {
        boolean stopOnFirst = false;
        return getContainers.getContainersForLocation(location, null, stopOnFirst);
    }

    //---------------------------------- PRIVATE API BELOW --------------------------------------------

    /**
     * Creates the editor input from a given path.
     *
     * @param path the path for the editor input we're looking
     * @param askIfDoesNotExist if true, it'll try to ask the user/check existing editors and look
     * in the workspace for matches given the name
     * @param project if provided, and if a matching file is found in this project, that file will be
     * opened before asking the user to select from a list of all matches
     *
     * @return the editor input found or none if None was available for the given path
     */
    public IEditorInput createEditorInput(IPath path, boolean askIfDoesNotExist, IPyStackFrame pyStackFrame,
            IProject project) {
        int onSourceNotFound = PySourceLocatorPrefs.getOnSourceNotFound();
        IEditorInput edInput = null;

        File systemFile = path.toFile();
        IEditorInput input = getEditorInputFromExistingEditors(systemFile);
        if (input != null) {
            return input;
        }

        String pathTranslation = PySourceLocatorPrefs.getPathTranslation(path);
        if (pathTranslation != null) {
            if (!pathTranslation.equals(PySourceLocatorPrefs.DONTASK)) {
                //change it for the registered translation
                path = Path.fromOSString(pathTranslation);
            } else {
                //DONTASK!!
                askIfDoesNotExist = false;
            }
        }

        IFile fileForLocation = getFileForLocation(path, project);
        if (fileForLocation != null && fileForLocation.exists()) {
            //if a project was specified, make sure the file found comes from that project
            return new FileEditorInput(fileForLocation);
        }

        //getFileForLocation() will search all projects starting with the one we pass and references,
        //so, if not found there, it is probably an external file
        if (systemFile.exists()) {
            edInput = PydevFileEditorInput.create(systemFile, true);
        }

        if (edInput == null) {
            //here we can do one more thing: if the file matches some opened editor, let's use it...
            //(this is done because when debugging, we don't want to be asked over and over
            //for the same file)
            input = getEditorInputFromExistingEditors(systemFile.getName());
            if (input != null) {
                return input;
            }

            if (askIfDoesNotExist
                    && (onSourceNotFound == PySourceLocatorPrefs.ASK_FOR_FILE || onSourceNotFound == PySourceLocatorPrefs.ASK_FOR_FILE_GET_FROM_SERVER)) {

                //this is the last resort... First we'll try to check for a 'good' match,
                //and if there's more than one we'll ask it to the user
                IWorkspace w = ResourcesPlugin.getWorkspace();
                List<IFile> likelyFiles = getLikelyFiles(path, w);
                IFile iFile = selectWorkspaceFile(likelyFiles.toArray(new IFile[0]));
                if (iFile != null) {
                    IPath location = iFile.getLocation();
                    if (location != null) {
                        PySourceLocatorPrefs.addPathTranslation(path, location);
                        return new FileEditorInput(iFile);
                    }
                }

                //ok, ask the user for any file in the computer
                IEditorInput pydevFileEditorInput = selectFilesystemFileForPath(path);
                input = pydevFileEditorInput;
                if (input != null) {
                    File file = PydevFileEditorInput.getFile(pydevFileEditorInput);
                    if (file != null) {
                        PySourceLocatorPrefs.addPathTranslation(path,
                                Path.fromOSString(FileUtils.getFileAbsolutePath(file)));
                        return input;
                    }
                }

                PySourceLocatorPrefs.setIgnorePathTranslation(path);
            }
        }

        if (edInput == null
                && (onSourceNotFound == PySourceLocatorPrefs.ASK_FOR_FILE_GET_FROM_SERVER || onSourceNotFound == PySourceLocatorPrefs.GET_FROM_SERVER)) {
            if (pyStackFrame != null) {
                try {
                    String fileContents = pyStackFrame.getFileContents();
                    if (fileContents != null && fileContents.length() > 0) {
                        String lastSegment = path.lastSegment();
                        File workspaceMetadataFile = PydevPlugin.getWorkspaceMetadataFile("temporary_files");
                        if (!workspaceMetadataFile.exists()) {
                            workspaceMetadataFile.mkdirs();
                        }
                        File file = new File(workspaceMetadataFile, lastSegment);
                        try {
                            if (file.exists()) {
                                file.delete();
                            }
                        } catch (Exception e) {
                        }
                        FileUtils.writeStrToFile(fileContents, file);
                        try {
                            file.setReadOnly();
                        } catch (Exception e) {
                        }
                        edInput = PydevFileEditorInput.create(file, true);
                    }

                } catch (Exception e) {
                    Log.log(e);
                }

            }
        }
        return edInput;
    }

    /**
     * @param matchFile the file to match in the editor
     * @return an editor input from an existing editor available
     */
    private IEditorInput getEditorInputFromExistingEditors(final File matchFile) {
        return new FindFromExistingEditorsFile(matchFile).findFromOpenedPyEdits();
    }

    /**
     * @param matchName the name to match in the editor
     * @return an editor input from an existing editor available
     */
    private IEditorInput getEditorInputFromExistingEditors(final String matchName) {
        return new FindFromExistingEditorsName(matchName).findFromOpenedPyEdits();
    }

    /**
     * This is the last resort... pointing to some filesystem file to get the editor for some path.
     */
    protected IEditorInput selectFilesystemFileForPath(final IPath path) {
        final List<String> l = new ArrayList<String>();
        Runnable r = new Runnable() {

            public void run() {
                Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
                FileDialog dialog = new FileDialog(shell);
                dialog.setText(path + " - select correspondent filesystem file.");
                String[] wildcardValidSourceFiles = FileTypesPreferencesPage.getWildcardValidSourceFiles();
                wildcardValidSourceFiles = StringUtils.addString(wildcardValidSourceFiles, "*");
                dialog.setFilterExtensions(wildcardValidSourceFiles);
                String string = dialog.open();
                if (string != null) {
                    l.add(string);
                }
            }
        };
        if (Display.getCurrent() == null) { //not ui-thread
            Display.getDefault().syncExec(r);
        } else {
            r.run();
        }
        if (l.size() > 0) {
            String fileAbsolutePath = FileUtils.getFileAbsolutePath(l.get(0));
            return PydevFileEditorInput.create(new File(fileAbsolutePath), true);
        }
        return null;
    }

    /**
     * This method will pass all the files in the workspace and check if there's a file that might
     * be a match to some path (use only as an almost 'last-resort').
     */
    private List<IFile> getLikelyFiles(IPath path, IWorkspace w) {
        List<IFile> ret = new ArrayList<IFile>();
        try {
            IResource[] resources = w.getRoot().members();
            getLikelyFiles(path, ret, resources);
        } catch (CoreException e) {
            Log.log(e);
        }
        return ret;
    }

    /**
     * Used to recursively get the likely files given the first set of containers
     */
    private void getLikelyFiles(IPath path, List<IFile> ret, IResource[] resources) throws CoreException {
        String strPath = path.removeFileExtension().lastSegment().toLowerCase(); //this will return something as 'foo'

        for (IResource resource : resources) {
            if (resource instanceof IFile) {
                IFile f = (IFile) resource;

                if (PythonPathHelper.isValidSourceFile(f)) {
                    if (resource.getFullPath().removeFileExtension().lastSegment().toLowerCase().equals(strPath)) {
                        ret.add((IFile) resource);
                    }
                }
            } else if (resource instanceof IContainer) {
                getLikelyFiles(path, ret, ((IContainer) resource).members());
            }
        }
    }

    /**
     * Ask the user to select one file of the given list of files (if some is available)
     *
     * @param files the files available for selection.
     * @return the selected file (from the files passed) or null if there was no file available for
     * selection or if the user canceled it.
     */
    private IFile selectWorkspaceFile(final IFile[] files) {
        if (files == null || files.length == 0) {
            return null;
        }
        if (files.length == 1) {
            return files[0];
        }
        final List<IFile> selected = new ArrayList<IFile>();

        Runnable r = new Runnable() {
            public void run() {
                Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
                ElementListSelectionDialog dialog = new ElementListSelectionDialog(shell, new PyFileLabelProvider());
                dialog.setElements(files);
                dialog.setTitle("Select Workspace File");
                dialog.setMessage("File may be matched to multiple files in the workspace.");
                if (dialog.open() == Window.OK) {
                    selected.add((IFile) dialog.getFirstResult());
                }
            }

        };
        if (Display.getCurrent() == null) { //not ui-thread
            Display.getDefault().syncExec(r);
        } else {
            r.run();
        }
        if (selected.size() > 0) {
            return selected.get(0);
        }
        return null;
    }

}
