/**
 * 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.editor;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.python.pydev.core.concurrency.SingleJobRunningPool;
import org.python.pydev.core.log.Log;
import org.python.pydev.plugin.PydevPlugin;
import org.python.pydev.plugin.nature.PythonNature;
import org.python.pydev.plugin.preferences.PyTitlePreferencesPage;
import org.python.pydev.shared_core.callbacks.ICallback0;
import org.python.pydev.shared_core.string.StringUtils;
import org.python.pydev.shared_core.structure.Tuple;
import org.python.pydev.shared_ui.utils.RunInUiThread;

/**
 * The whole picture:
 *
 * 1. In django it's common to have multiple files with the same name
 *
 * 2. __init__ files are everywhere
 *
 * We need a way to uniquely identify those.
 *
 * Options:
 *
 * - For __init__ files, an option would be having a different icon and adding the package
 * name instead of the __init__ (so if an __init__ is under my_package, we would show
 * only 'my_package' and would change the icon for the opened editor).
 *
 * - For the default django files (models.py, settings.py, tests.py, views.py), we could use
 * the same approach -- in fact, make that configurable!
 *
 * - For any file (including the cases above), if the name would end up being duplicated, change
 * the title so that all names are always unique (note that the same name may still be used if
 * the icon is different).
 */
/*default*/final class PyEditTitle implements IPropertyChangeListener {

    /**
     * Singleton access for the title management.
     */
    private static PyEditTitle singleton;

    /**
     * Lock for accessing the singleton.
     */
    private static Object lock = new Object();

    /**
     * Helper to ensure that only a given job is running at some time.
     */
    private SingleJobRunningPool jobPool = new SingleJobRunningPool();

    private PyEditTitle() {
        IPreferenceStore preferenceStore = PydevPlugin.getDefault().getPreferenceStore();
        preferenceStore.addPropertyChangeListener(this);
    }

    @Override
    public void propertyChange(PropertyChangeEvent event) {
        //When the
        String property = event.getProperty();
        if (PyTitlePreferencesPage.isTitlePreferencesProperty(property)) {

            Job job = new Job("Invalidate title") {

                @Override
                protected IStatus run(IProgressMonitor monitor) {
                    try {
                        List<IEditorReference> currentEditorReferences;
                        do {
                            currentEditorReferences = getCurrentEditorReferences();
                            synchronized (this) {
                                try {
                                    Thread.sleep(200);
                                } catch (InterruptedException e) {
                                    //ignore.
                                }
                            }
                        } while (PydevPlugin.isAlive() && currentEditorReferences == null); //stop trying if the plugin is stopped;

                        if (currentEditorReferences != null) {
                            final List<IEditorReference> refs = currentEditorReferences;
                            RunInUiThread.sync(new Runnable() {

                                @Override
                                public void run() {
                                    for (final IEditorReference iEditorReference : refs) {
                                        final IEditorPart editor = iEditorReference.getEditor(true);
                                        if (editor instanceof PyEdit) {
                                            try {
                                                invalidateTitle((PyEdit) editor, iEditorReference.getEditorInput());
                                            } catch (PartInitException e) {
                                                //ignore
                                            }
                                        }
                                    }
                                }
                            });
                        }

                    } finally {
                        jobPool.removeJob(this);
                    }
                    return Status.OK_STATUS;
                }
            };
            job.setPriority(Job.SHORT);
            jobPool.addJob(job);
        }
    }

    /**
     * This method will update the title of all the editors that have a title that would match
     * the passed input.
     *
     * Note that on the first try it will update the images of all editors.
     * @param pyEdit
     */
    public static void invalidateTitle(PyEdit pyEdit, IEditorInput input) {
        synchronized (lock) {
            boolean createdSingleton = false;
            if (singleton == null) {
                singleton = new PyEditTitle();
                createdSingleton = true;

            }
            //updates the title and image for the passed input.
            singleton.invalidateTitleInput(pyEdit, input);

            if (createdSingleton) {
                //In the first time, we need to invalidate all icons (because eclipse doesn't restore them the way we left them).
                //Note that we don't need to do that for titles because those are properly saved on close/restore.
                singleton.restoreAllPydevEditorsWithDifferentIcon();
            }
        }
    }

    /**
     * Sadly, we have to restore all pydev editors that have a different icon to make it correct.
     *
     * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=308740
     */
    private void restoreAllPydevEditorsWithDifferentIcon() {
        if (!PyTitlePreferencesPage.useCustomInitIcon()) {
            return;
        }
        Job job = new Job("Invalidate images") {

            @Override
            protected IStatus run(IProgressMonitor monitor) {
                try {
                    while (PydevPlugin.isAlive() && !doRestoreAllPydevEditorsWithDifferentIcons()) { //stop trying if the plugin is stopped
                        synchronized (this) {
                            try {
                                Thread.sleep(200);
                            } catch (InterruptedException e) {
                                //ignore.
                            }
                        }
                    }
                    ;
                } finally {
                    jobPool.removeJob(this);
                }
                return Status.OK_STATUS;
            }
        };
        job.setPriority(Job.SHORT);
        jobPool.addJob(job);
    }

    /**
     * Sadly, we have to restore all pydev editors to make the icons correct.
     *
     * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=308740
     */
    private boolean doRestoreAllPydevEditorsWithDifferentIcons() {
        if (!PyTitlePreferencesPage.useCustomInitIcon()) {
            return true; //Ok, nothing custom!
        }

        final List<IEditorReference> editorReferences = getCurrentEditorReferences();
        if (editorReferences == null) {
            //couldn't be gotten.
            return false;
        }
        //Update images

        RunInUiThread.async(new Runnable() {

            @Override
            public void run() {
                for (IEditorReference iEditorReference : editorReferences) {
                    try {
                        if (iEditorReference != null) {
                            IPath pathFromInput = getPathFromInput(iEditorReference.getEditorInput());
                            if (pathFromInput != null) {
                                String lastSegment = pathFromInput.lastSegment();
                                if (lastSegment != null
                                        && (lastSegment.startsWith("__init__.") || PyTitlePreferencesPage
                                                .isDjangoModuleToDecorate(lastSegment))) {
                                    iEditorReference.getEditor(true); //restore it.
                                }
                            }
                        }
                    } catch (PartInitException e) {
                        //ignore
                    }

                    //Note, removed the code below -- just restoring the editor is enough and the
                    //only way to make it work for now. See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=308740

                    //			try {
                    //				IEditorInput input = iEditorReference.getEditorInput();
                    //				IPath path = getPathFromInput(input);
                    //				updateImage(null, iEditorReference, path);
                    //			} catch (PartInitException e) {
                    //				//ignore
                    //			}
                }
            }
        });

        return true;
    }

    /**
     * Updates the title text and image of the given pyEdit (based on the passed input).
     *
     * That will be done depending on the other open editors (if the user has chosen
     * unique names).
     */
    private void invalidateTitleInput(final PyEdit pyEdit, final IEditorInput input) {
        if (input == null) {
            return;
        }

        final IPath pathFromInput = getPathFromInput(input);
        if (pathFromInput == null || pathFromInput.segmentCount() == 0) {
            return; //not much we can do!
        }

        final String lastSegment = pathFromInput.lastSegment();
        if (lastSegment == null) {
            return;
        }

        final String initHandling = PyTitlePreferencesPage.getInitHandling();
        final String djangoModulesHandling = PyTitlePreferencesPage.getDjangoModulesHandling();

        //initially set this as the title (and change it later to a computed name).
        String computedEditorTitle = getPartNameInLevel(1, pathFromInput, initHandling, djangoModulesHandling,
                input).o1;

        pyEdit.setEditorTitle(computedEditorTitle);
        updateImage(pyEdit, null, pathFromInput);

        if (!PyTitlePreferencesPage.getEditorNamesUnique()) {
            return; //the user accepts having the same name for 2 files, no more work to do.
        }

        Job job = new Job("Invalidate title") {

            @Override
            protected IStatus run(IProgressMonitor monitor) {
                try {
                    while (PydevPlugin.isAlive()
                            && !initializeTitle(pyEdit, input, pathFromInput, lastSegment, initHandling,
                                    djangoModulesHandling)) { //stop trying if the plugin is stopped
                        synchronized (this) {
                            try {
                                Thread.sleep(200);
                            } catch (InterruptedException e) {
                                //ignore.
                            }
                        }
                    }
                    ;
                } finally {
                    jobPool.removeJob(this);
                }
                return Status.OK_STATUS;
            }
        };
        job.setPriority(Job.SHORT);
        jobPool.addJob(job);

    }

    /**
     * Updates the image of the passed editor.
     */
    private void updateImage(PyEdit pyEdit, IEditorReference iEditorReference, IPath path) {
        String lastSegment = path.lastSegment();
        if (lastSegment != null) {
            if (lastSegment.startsWith("__init__.")) {
                Image initIcon = PyTitlePreferencesPage.getInitIcon();
                if (initIcon != null) {
                    if (pyEdit != null) {
                        pyEdit.setEditorImage(initIcon);
                    } else {
                        setEditorReferenceImage(iEditorReference, initIcon);
                    }
                }

            } else if (PyTitlePreferencesPage.isDjangoModuleToDecorate(lastSegment)) {
                try {
                    IEditorInput editorInput;
                    if (pyEdit != null) {
                        editorInput = pyEdit.getEditorInput();
                    } else {
                        editorInput = iEditorReference.getEditorInput();
                    }
                    if (isDjangoHandledModule(PyTitlePreferencesPage.getDjangoModulesHandling(), editorInput,
                            lastSegment)) {
                        Image image = PyTitlePreferencesPage.getDjangoModuleIcon(lastSegment);
                        if (pyEdit != null) {
                            pyEdit.setEditorImage(image);
                        } else {
                            setEditorReferenceImage(iEditorReference, image);
                        }
                    }
                } catch (PartInitException e) {
                    //ignore
                }
            }
        }
    }

    /**
     * 2 pydev editors should never have the same title, so, this method will make sure that
     * this won't happen.
     *
     * @return true if it was able to complete and false if some requisite is not available.
     */
    private boolean initializeTitle(final PyEdit pyEdit, IEditorInput input, final IPath pathFromInput,
            String lastSegment, String initHandling, String djangoModulesHandling) {

        List<IEditorReference> editorReferences = getCurrentEditorReferences();
        if (editorReferences == null) {
            //couldn't be gotten.
            return false;
        }

        Map<IPath, List<IEditorReference>> partsAndPaths = removeEditorsNotMatchingCurrentName(lastSegment,
                editorReferences);

        if (partsAndPaths.size() > 1) {
            ArrayList<IPath> keys = new ArrayList<IPath>(partsAndPaths.keySet());

            //There are other editors with the same name... (1 is the editor that requested the change)
            //let's make them unique!
            int level = 0;
            List<String> names = new ArrayList<String>();
            Map<String, Integer> allNames = new HashMap<String, Integer>();

            do {
                names.clear();
                allNames.clear();
                level++;
                for (int i = 0; i < keys.size(); i++) {
                    IPath path = keys.get(i);
                    List<IEditorReference> refs = partsAndPaths.get(path);
                    if (refs == null || refs.size() == 0) {
                        Log.log("Unexpected condition. Key path without related editors: " + path);
                        keys.remove(i);
                        i--; //make up for the removed editor.
                        continue;
                    }

                    IEditorInput editorInput;
                    try {
                        //Note that we're only getting one reference here for the editor input.
                        //The problem is that for knowing about django, we use this editor, which means that
                        //we may have a bug in which if we have a file that's within a django project and
                        //another out of a django project, we'll leave by chance how it'll be displayed... seems
                        //a bit exoteric in real cases, but if it happens this behaviour may need to be changed.
                        editorInput = refs.get(0).getEditorInput();
                    } catch (PartInitException e) {
                        continue;
                    }
                    Tuple<String, Boolean> nameAndReachedMax = getPartNameInLevel(level, path, initHandling,
                            djangoModulesHandling, editorInput);
                    if (nameAndReachedMax.o2) { //maximum level reached for path
                        setEditorReferenceTitle(refs, nameAndReachedMax.o1);
                        keys.remove(i);
                        i--; //make up for the removed editor.
                        continue;
                    }
                    names.add(nameAndReachedMax.o1);
                    Integer count = allNames.get(nameAndReachedMax.o1);
                    if (count == null) {
                        allNames.put(nameAndReachedMax.o1, 1);
                    } else {
                        allNames.put(nameAndReachedMax.o1, count + 1);
                    }
                }
                for (int i = 0; i < keys.size(); i++) {
                    String finalName = names.get(i);
                    Integer count = allNames.get(finalName);
                    if (count == 1) { //no duplicate found
                        IPath path = keys.get(i);
                        List<IEditorReference> refs = partsAndPaths.get(path);
                        setEditorReferenceTitle(refs, finalName);

                        keys.remove(i);
                        names.remove(i);
                        allNames.remove(finalName);
                        i--; //make up for the removed editor.
                    }
                }
            } while (allNames.size() > 0);
        }
        return true;
    }

    /**
     * @return a list of all the editors that have the last segment as 'currentName'
     */
    private Map<IPath, List<IEditorReference>> removeEditorsNotMatchingCurrentName(String currentName,
            List<IEditorReference> editorReferences) {
        Map<IPath, List<IEditorReference>> ret = new HashMap<IPath, List<IEditorReference>>();
        for (Iterator<IEditorReference> it = editorReferences.iterator(); it.hasNext();) {
            IEditorReference iEditorReference = it.next();
            try {
                IEditorInput otherInput = iEditorReference.getEditorInput();

                //Always get the 'original' name and not the currently set name, because
                //if we previously had an __init__.py editor which we renamed to package/__init__.py
                //and we open a new __init__.py, we want it renamed to new_package/__init__.py
                IPath pathFromOtherInput = getPathFromInput(otherInput);
                if (pathFromOtherInput == null) {
                    continue;
                }

                String lastSegment = pathFromOtherInput.lastSegment();
                if (lastSegment == null) {
                    continue;
                }

                if (!currentName.equals(lastSegment)) {
                    continue;
                }
                List<IEditorReference> list = ret.get(pathFromOtherInput);
                if (list == null) {
                    list = new ArrayList<IEditorReference>();
                    ret.put(pathFromOtherInput, list);
                }
                list.add(iEditorReference);
            } catch (Throwable e) {
                Log.log(e);
            }
        }
        return ret;
    }

    /**
     * @return the current editor references or null if no editor references are available.
     *
     * Note that this method may be slow as it will need UI access (which is asynchronously
     * gotten)
     */
    private List<IEditorReference> getCurrentEditorReferences() {
        final List<IEditorReference[]> editorReferencesFound = new ArrayList<IEditorReference[]>();

        RunInUiThread.async(new Runnable() {

            @Override
            public void run() {
                IEditorReference[] found = null;
                try {
                    IWorkbenchWindow workbenchWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
                    if (workbenchWindow == null) {
                        return;
                    }
                    IWorkbenchPage activePage = workbenchWindow.getActivePage();
                    if (activePage == null) {
                        return;
                    }
                    found = activePage.getEditorReferences();
                } finally {
                    editorReferencesFound.add(found);
                }
            }
        });
        while (editorReferencesFound.size() == 0) {
            synchronized (this) {
                try {
                    wait(10);
                } catch (InterruptedException e) {
                    //ignore
                }
            }
        }
        IEditorReference[] editorReferences = editorReferencesFound.get(0);
        if (editorReferences == null) {
            return null;
        }
        ArrayList<IEditorReference> ret = new ArrayList<IEditorReference>();
        for (IEditorReference iEditorReference : editorReferences) {
            if (!PyEdit.EDITOR_ID.equals(iEditorReference.getId())) {
                continue; //only analyze Pydev editors
            }
            ret.add(iEditorReference);
        }
        return ret;
    }

    /**
     * Sets the image of the passed editor reference. Will try to restore the editor for
     * doing that.
     *
     * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=308740
     */
    private void setEditorReferenceImage(final IEditorReference iEditorReference, final Image image) {
        RunInUiThread.async(new Runnable() {

            @Override
            public void run() {
                try {
                    IEditorPart editor = iEditorReference.getEditor(true);
                    if (editor instanceof PyEdit) {
                        ((PyEdit) editor).setEditorImage(image);
                    }

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

    /**
     * Sets the title of the passed editor reference. Will try to restore the editor for
     * doing that.
     *
     * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=308740
     */
    private void setEditorReferenceTitle(final List<IEditorReference> refs, final String title) {
        if (refs == null) {
            return;
        }
        final int size = refs.size();
        if (size == 1) {
            for (final IEditorReference ref : refs) {

                if (title.equals(ref.getTitle())) {
                    //Nothing to do if it's already the same.
                    return;
                }

                RunInUiThread.async(new Runnable() {

                    @Override
                    public void run() {
                        try {
                            IEditorPart editor = ref.getEditor(true);
                            if (editor instanceof PyEdit) {
                                ((PyEdit) editor).setEditorTitle(title);
                            }
                        } catch (Throwable e) {
                            Log.log(e);
                        }
                    }
                });
            }
        } else if (size > 1) {

            RunInUiThread.async(new Runnable() {

                @Override
                public void run() {
                    try {
                        final String key = "PyEditTitleEditorNumberSet";
                        final Set<Integer> used = new HashSet<Integer>();
                        final ArrayList<PyEdit> toSet = new ArrayList<PyEdit>();

                        for (final IEditorReference ref : refs) {
                            PyEdit editor = (PyEdit) ref.getEditor(true);
                            Integer curr = (Integer) editor.cache.get(key);
                            if (curr != null) {
                                used.add(curr);
                                editor.setEditorTitle(title + " #" + (curr + 1));
                            } else {
                                toSet.add(editor);
                            }
                        }

                        //Calculate the next integer available
                        final ICallback0<Integer> next = new ICallback0<Integer>() {
                            private int last = 0;

                            @Override
                            public Integer call() {
                                for (; last < Integer.MAX_VALUE; last++) {
                                    if (used.contains(last)) {
                                        continue;
                                    }
                                    used.add(last);
                                    return last;
                                }
                                throw new AssertionError("Should not get here");
                            }
                        };

                        //If it got here in toSet, it still must be set!
                        for (PyEdit editor : toSet) {
                            Integer i = next.call();
                            editor.setEditorTitle(title + " #" + (i + 1));
                        }
                    } catch (Throwable e) {
                        Log.log(e);
                    }
                }
            });
        }
    }

    /**
     * @param input
     * @return a tuple with the part name to be used and a boolean indicating if the maximum level
     * has been reached for this path.
     */
    private Tuple<String, Boolean> getPartNameInLevel(int level, IPath path, String initHandling,
            String djangoModulesHandling, IEditorInput input) {
        String name = input.getName();
        String[] segments = path.segments();
        if (segments.length == 0) {
            return new Tuple<String, Boolean>("", true);
        }
        if (segments.length == 1) {
            return new Tuple<String, Boolean>(segments[1], true);
        }

        boolean handled = isDjangoHandledModule(djangoModulesHandling, input, name);
        if (handled
                && djangoModulesHandling == PyTitlePreferencesPage.TITLE_EDITOR_DJANGO_MODULES_SHOW_PARENT_AND_DECORATE) {
            String[] dest = new String[segments.length - 1];
            System.arraycopy(segments, 0, dest, 0, dest.length);
            segments = dest;

        } else if (initHandling != PyTitlePreferencesPage.TITLE_EDITOR_INIT_HANDLING_IN_TITLE) {
            if (name.startsWith("__init__.")) {
                //remove the __init__.
                String[] dest = new String[segments.length - 1];
                System.arraycopy(segments, 0, dest, 0, dest.length);
                segments = dest;
                if (dest.length > 0) {
                    name = dest[dest.length - 1];
                }
            }
        }

        int startAt = segments.length - level;
        if (startAt < 0) {
            startAt = 0;
        }

        int endAt = segments.length - 1;

        String modulePart = StringUtils.join(".", segments, startAt, endAt);

        if (!PyTitlePreferencesPage.getTitleShowExtension()) {
            int i = name.lastIndexOf('.');
            if (i != -1) {
                name = name.substring(0, i);
            }
        }
        if (modulePart.length() > 0) {
            return new Tuple<String, Boolean>(name + " (" + modulePart + ")", startAt == 0);
        } else {
            return new Tuple<String, Boolean>(name, startAt == 0);
        }
    }

    private boolean isDjangoHandledModule(String djangoModulesHandling, IEditorInput input, String lastSegment) {
        boolean handled = false;
        if (djangoModulesHandling == PyTitlePreferencesPage.TITLE_EDITOR_DJANGO_MODULES_SHOW_PARENT_AND_DECORATE
                || djangoModulesHandling == PyTitlePreferencesPage.TITLE_EDITOR_DJANGO_MODULES_DECORATE) {

            if (input instanceof IFileEditorInput) {
                IFileEditorInput iFileEditorInput = (IFileEditorInput) input;
                IFile file = iFileEditorInput.getFile();
                IProject project = file.getProject();
                try {
                    if (project.hasNature(PythonNature.DJANGO_NATURE_ID)) {
                        if (PyTitlePreferencesPage.isDjangoModuleToDecorate(lastSegment)) {
                            //remove the module name.
                            handled = true;
                        }
                    }
                } catch (CoreException e) {
                    Log.log(e);
                }
            }
        }
        return handled;
    }

    /**
     * @return This is the Path that the editor is editing.
     */
    private IPath getPathFromInput(IEditorInput otherInput) {
        IPath path = null;
        if (otherInput instanceof IPathEditorInput) {
            IPathEditorInput iPathEditorInput = (IPathEditorInput) otherInput;
            try {
                path = iPathEditorInput.getPath();
            } catch (IllegalArgumentException e) {
                //ignore: we may have the trace below inside the FileEditorInput.
                //java.lang.IllegalArgumentException
                //at org.eclipse.ui.part.FileEditorInput.getPath(FileEditorInput.java:208)
                //at org.python.pydev.editor.PyEditTitle.getPathFromInput(PyEditTitle.java:751)

            }
        }
        if (path == null) {
            if (otherInput instanceof IFileEditorInput) {
                IFileEditorInput iFileEditorInput = (IFileEditorInput) otherInput;
                path = iFileEditorInput.getFile().getFullPath();
            }
        }
        if (path == null) {
            try {
                if (otherInput instanceof IURIEditorInput) {
                    IURIEditorInput iuriEditorInput = (IURIEditorInput) otherInput;
                    path = Path.fromOSString(new File(iuriEditorInput.getURI()).toString());
                }
            } catch (Throwable e) {
                //Ignore (IURIEditorInput not available on 3.2)
            }
        }
        return path;
    }

}
