/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ide.eclipse.adt.internal.resources.manager;

import com.android.ide.common.resources.ResourceFile;
import com.android.ide.common.resources.ResourceFolder;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;

import java.util.ArrayList;

/**
 * The Global Project Monitor tracks project file changes, and forward them to simple project,
 * file, and folder listeners.
 * Those listeners can be setup with masks to listen to particular events.
 * <p/>
 * To track project resource changes, use the monitor in the {@link ResourceManager}. It is more
 * efficient and while the global ProjectMonitor can track any file, deleted resource files
 * cannot be matched to previous {@link ResourceFile} or {@link ResourceFolder} objects by the
 * time the listeners get the event notifications.
 *
 * @see IProjectListener
 * @see IFolderListener
 * @see IFileListener
 */
public final class GlobalProjectMonitor {

    private final static GlobalProjectMonitor sThis = new GlobalProjectMonitor();

    /**
     * Classes which implement this interface provide a method that deals
     * with file change events.
     */
    public interface IFileListener {
        /**
         * Sent when a file changed.
         * @param file The file that changed.
         * @param markerDeltas The marker deltas for the file.
         * @param kind The change kind. This is equivalent to
         * {@link IResourceDelta#accept(IResourceDeltaVisitor)}
         */
        public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind);
    }

    /**
     * Classes which implements this interface provide methods dealing with project events.
     */
    public interface IProjectListener {
        /**
         * Sent for each opened android project at the time the listener is put in place.
         * @param project the opened project.
         */
        public void projectOpenedWithWorkspace(IProject project);
        /**
         * Sent when a project is opened.
         * @param project the project being opened.
         */
        public void projectOpened(IProject project);
        /**
         * Sent when a project is closed.
         * @param project the project being closed.
         */
        public void projectClosed(IProject project);
        /**
         * Sent when a project is deleted.
         * @param project the project about to be deleted.
         */
        public void projectDeleted(IProject project);

        /**
         * Sent when a project is renamed. During a project rename
         * {@link #projectDeleted(IProject)} and {@link #projectOpened(IProject)} are also called.
         * This is called last.
         *
         * @param project the new {@link IProject} object.
         * @param from the path of the project before the rename action.
         */
        public void projectRenamed(IProject project, IPath from);
    }

    /**
     * Classes which implement this interface provide a method that deals
     * with folder change events
     */
    public interface IFolderListener {
        /**
         * Sent when a folder changed.
         * @param folder The file that was changed
         * @param kind The change kind. This is equivalent to {@link IResourceDelta#getKind()}
         */
        public void folderChanged(IFolder folder, int kind);
    }

    /**
     * Interface for a listener to be notified when resource change event starts and ends.
     */
    public interface IResourceEventListener {
        public void resourceChangeEventStart();
        public void resourceChangeEventEnd();
    }

    /**
     * Interface for a listener that gets passed the raw delta without processing.
     *
     */
    public interface IRawDeltaListener {
        public void visitDelta(IResourceDelta delta);
    }

    /**
     * Base listener bundle to associate a listener to an event mask.
     */
    private static class ListenerBundle {
        /** Mask value to accept all events */
        public final static int MASK_NONE = -1;

        /**
         * Event mask. Values accepted are IResourceDelta.###
         * @see IResourceDelta#ADDED
         * @see IResourceDelta#REMOVED
         * @see IResourceDelta#CHANGED
         * @see IResourceDelta#ADDED_PHANTOM
         * @see IResourceDelta#REMOVED_PHANTOM
         * */
        int kindMask;
    }

    /**
     * Listener bundle for file event.
     */
    private static class FileListenerBundle extends ListenerBundle {

        /** The file listener */
        IFileListener listener;
    }

    /**
     * Listener bundle for folder event.
     */
    private static class FolderListenerBundle extends ListenerBundle {
        /** The file listener */
        IFolderListener listener;
    }

    private final ArrayList<FileListenerBundle> mFileListeners =
        new ArrayList<FileListenerBundle>();

    private final ArrayList<FolderListenerBundle> mFolderListeners =
        new ArrayList<FolderListenerBundle>();

    private final ArrayList<IProjectListener> mProjectListeners = new ArrayList<IProjectListener>();

    private final ArrayList<IResourceEventListener> mEventListeners =
        new ArrayList<IResourceEventListener>();

    private final ArrayList<IRawDeltaListener> mRawDeltaListeners =
        new ArrayList<IRawDeltaListener>();

    private IWorkspace mWorkspace;

    /**
     * Delta visitor for resource changes.
     */
    private final class DeltaVisitor implements IResourceDeltaVisitor {

        public boolean visit(IResourceDelta delta) {
            // notify the raw delta listeners
            for (IRawDeltaListener listener : mRawDeltaListeners) {
                listener.visitDelta(delta);
            }
            // Find the other resource listeners to notify
            IResource r = delta.getResource();
            int type = r.getType();
            if (type == IResource.FILE) {
                int kind = delta.getKind();
                // notify the listeners.
                for (FileListenerBundle bundle : mFileListeners) {
                    if (bundle.kindMask == ListenerBundle.MASK_NONE
                            || (bundle.kindMask & kind) != 0) {
                        try {
                            bundle.listener.fileChanged((IFile)r, delta.getMarkerDeltas(), kind);
                        } catch (Throwable t) {
                            AdtPlugin.log(t,"Failed to call IFileListener.fileChanged");
                        }
                    }
                }
                return false;
            } else if (type == IResource.FOLDER) {
                int kind = delta.getKind();
                // notify the listeners.
                for (FolderListenerBundle bundle : mFolderListeners) {
                    if (bundle.kindMask == ListenerBundle.MASK_NONE
                            || (bundle.kindMask & kind) != 0) {
                        try {
                            bundle.listener.folderChanged((IFolder)r, kind);
                        } catch (Throwable t) {
                            AdtPlugin.log(t,"Failed to call IFileListener.folderChanged");
                        }
                    }
                }
                return true;
            } else if (type == IResource.PROJECT) {
                int flags = delta.getFlags();

                if ((flags & IResourceDelta.OPEN) != 0) {
                    // the project is opening or closing.
                    IProject project = (IProject)r;

                    if (project.isOpen()) {
                        // notify the listeners.
                        for (IProjectListener pl : mProjectListeners) {
                            try {
                                pl.projectOpened(project);
                            } catch (Throwable t) {
                                AdtPlugin.log(t,"Failed to call IProjectListener.projectOpened");
                            }
                        }
                    } else {
                        // notify the listeners.
                        for (IProjectListener pl : mProjectListeners) {
                            try {
                                pl.projectClosed(project);
                            } catch (Throwable t) {
                                AdtPlugin.log(t,"Failed to call IProjectListener.projectClosed");
                            }
                        }
                    }

                    if ((flags & IResourceDelta.MOVED_FROM) != 0) {
                        IPath from = delta.getMovedFromPath();
                        // notify the listeners.
                        for (IProjectListener pl : mProjectListeners) {
                            try {
                                pl.projectRenamed(project, from);
                            } catch (Throwable t) {
                                AdtPlugin.log(t,"Failed to call IProjectListener.projectRenamed");
                            }
                        }
                    }
                }
            }

            return true;
        }
    }

    public static GlobalProjectMonitor getMonitor() {
        return sThis;
    }


    /**
     * Starts the resource monitoring.
     * @param ws The current workspace.
     * @return The monitor object.
     */
    public static GlobalProjectMonitor startMonitoring(IWorkspace ws) {
        if (sThis != null) {
            ws.addResourceChangeListener(sThis.mResourceChangeListener,
                    IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_DELETE);
            sThis.mWorkspace = ws;
        }
        return sThis;
    }

    /**
     * Stops the resource monitoring.
     * @param ws The current workspace.
     */
    public static void stopMonitoring(IWorkspace ws) {
        if (sThis != null) {
            ws.removeResourceChangeListener(sThis.mResourceChangeListener);

            synchronized (sThis) {
                sThis.mFileListeners.clear();
                sThis.mProjectListeners.clear();
            }
        }
    }

    /**
     * Adds a file listener.
     * @param listener The listener to receive the events.
     * @param kindMask The event mask to filter out specific events.
     * {@link ListenerBundle#MASK_NONE} will forward all events.
     * See {@link ListenerBundle#kindMask} for more values.
     */
    public synchronized void addFileListener(IFileListener listener, int kindMask) {
        FileListenerBundle bundle = new FileListenerBundle();
        bundle.listener = listener;
        bundle.kindMask = kindMask;

        mFileListeners.add(bundle);
    }

    /**
     * Removes an existing file listener.
     * @param listener the listener to remove.
     */
    public synchronized void removeFileListener(IFileListener listener) {
        for (int i = 0 ; i < mFileListeners.size() ; i++) {
            FileListenerBundle bundle = mFileListeners.get(i);
            if (bundle.listener == listener) {
                mFileListeners.remove(i);
                return;
            }
        }
    }

    /**
     * Adds a folder listener.
     * @param listener The listener to receive the events.
     * @param kindMask The event mask to filter out specific events.
     * {@link ListenerBundle#MASK_NONE} will forward all events.
     * See {@link ListenerBundle#kindMask} for more values.
     */
    public synchronized void addFolderListener(IFolderListener listener, int kindMask) {
        FolderListenerBundle bundle = new FolderListenerBundle();
        bundle.listener = listener;
        bundle.kindMask = kindMask;

        mFolderListeners.add(bundle);
    }

    /**
     * Removes an existing folder listener.
     * @param listener the listener to remove.
     */
    public synchronized void removeFolderListener(IFolderListener listener) {
        for (int i = 0 ; i < mFolderListeners.size() ; i++) {
            FolderListenerBundle bundle = mFolderListeners.get(i);
            if (bundle.listener == listener) {
                mFolderListeners.remove(i);
                return;
            }
        }
    }

    /**
     * Adds a project listener.
     * @param listener The listener to receive the events.
     */
    public synchronized void addProjectListener(IProjectListener listener) {
        mProjectListeners.add(listener);

        // we need to look at the opened projects and give them to the listener.

        // get the list of opened android projects.
        IWorkspaceRoot workspaceRoot = mWorkspace.getRoot();
        IJavaModel javaModel = JavaCore.create(workspaceRoot);
        IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(javaModel,
                null /*filter*/);

        for (IJavaProject androidProject : androidProjects) {
            listener.projectOpenedWithWorkspace(androidProject.getProject());
        }
    }

    /**
     * Removes an existing project listener.
     * @param listener the listener to remove.
     */
    public synchronized void removeProjectListener(IProjectListener listener) {
        mProjectListeners.remove(listener);
    }

    /**
     * Adds a resource event listener.
     * @param listener The listener to receive the events.
     */
    public synchronized void addResourceEventListener(IResourceEventListener listener) {
        mEventListeners.add(listener);
    }

    /**
     * Removes an existing Resource Event listener.
     * @param listener the listener to remove.
     */
    public synchronized void removeResourceEventListener(IResourceEventListener listener) {
        mEventListeners.remove(listener);
    }

    /**
     * Adds a raw delta listener.
     * @param listener The listener to receive the deltas.
     */
    public synchronized void addRawDeltaListener(IRawDeltaListener listener) {
        mRawDeltaListeners.add(listener);
    }

    /**
     * Removes an existing Raw Delta listener.
     * @param listener the listener to remove.
     */
    public synchronized void removeRawDeltaListener(IRawDeltaListener listener) {
        mRawDeltaListeners.remove(listener);
    }

    private final IResourceChangeListener mResourceChangeListener = new IResourceChangeListener() {
        /**
         * Processes the workspace resource change events.
         *
         * @see IResourceChangeListener#resourceChanged(IResourceChangeEvent)
         */
        public synchronized void resourceChanged(IResourceChangeEvent event) {
            // notify the event listeners of a start.
            for (IResourceEventListener listener : mEventListeners) {
                try {
                    listener.resourceChangeEventStart();
                } catch (Throwable t) {
                    AdtPlugin.log(t,"Failed to call IResourceEventListener.resourceChangeEventStart");
                }
            }

            if (event.getType() == IResourceChangeEvent.PRE_DELETE) {
                // a project is being deleted. Lets get the project object and remove
                // its compiled resource list.
                IResource r = event.getResource();
                IProject project = r.getProject();

                // notify the listeners.
                for (IProjectListener pl : mProjectListeners) {
                    try {
                        pl.projectDeleted(project);
                    } catch (Throwable t) {
                        AdtPlugin.log(t,"Failed to call IProjectListener.projectDeleted");
                    }
                }
            } else {
                // this a regular resource change. We get the delta and go through it with a visitor.
                IResourceDelta delta = event.getDelta();

                DeltaVisitor visitor = new DeltaVisitor();
                try {
                    delta.accept(visitor);
                } catch (CoreException e) {
                }
            }

            // we're done, notify the event listeners.
            for (IResourceEventListener listener : mEventListeners) {
                try {
                    listener.resourceChangeEventEnd();
                } catch (Throwable t) {
                    AdtPlugin.log(t,"Failed to call IResourceEventListener.resourceChangeEventEnd");
                }
            }
        }
    };

}
