/*
 * 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.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AndroidConstants;
import com.android.ide.eclipse.adt.internal.resources.ResourceType;
import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFolderListener;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
import com.android.ide.eclipse.adt.internal.resources.manager.files.FileWrapper;
import com.android.ide.eclipse.adt.internal.resources.manager.files.FolderWrapper;
import com.android.ide.eclipse.adt.internal.resources.manager.files.IAbstractFile;
import com.android.ide.eclipse.adt.internal.resources.manager.files.IAbstractFolder;
import com.android.ide.eclipse.adt.internal.resources.manager.files.IFileWrapper;
import com.android.ide.eclipse.adt.internal.resources.manager.files.IFolderWrapper;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;

import org.eclipse.core.resources.IContainer;
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.IResourceDelta;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * The ResourceManager tracks resources for all opened projects.
 * <p/>
 * It provide direct access to all the resources of a project as a {@link ProjectResources}
 * object that allows accessing the resources through their file representation or as Android
 * resources (similar to what is seen by an Android application).
 * <p/>
 * The ResourceManager automatically tracks file changes to update its internal representation
 * of the resources so that they are always up to date.
 * <p/>
 * It also gives access to a monitor that is more resource oriented than the
 * {@link GlobalProjectMonitor}.
 * This monitor will let you track resource changes by giving you direct access to
 * {@link ResourceFile}, or {@link ResourceFolder}.
 *
 * @see ProjectResources
 */
public final class ResourceManager {

    private final static ResourceManager sThis = new ResourceManager();

    /** List of the qualifier object helping for the parsing of folder names */
    private final ResourceQualifier[] mQualifiers;

    /**
     * Map associating project resource with project objects.
     */
    private final HashMap<IProject, ProjectResources> mMap =
        new HashMap<IProject, ProjectResources>();

    /**
     * Interface to be notified of resource changes.
     *
     * @see ResourceManager#addListener(IResourceListener)
     * @see ResourceManager#removeListener(IResource)
     */
    public interface IResourceListener {
        /**
         * Notification for resource file change.
         * @param project the project of the file.
         * @param file the {@link ResourceFile} representing the file.
         * @param eventType the type of event. See {@link IResourceDelta}.
         */
        void fileChanged(IProject project, ResourceFile file, int eventType);
        /**
         * Notification for resource folder change.
         * @param project the project of the file.
         * @param folder the {@link ResourceFolder} representing the folder.
         * @param eventType the type of event. See {@link IResourceDelta}.
         */
        void folderChanged(IProject project, ResourceFolder folder, int eventType);
    }

    private final ArrayList<IResourceListener> mListeners = new ArrayList<IResourceListener>();

    /**
     * Sets up the resource manager with the global project monitor.
     * @param monitor The global project monitor
     */
    public static void setup(GlobalProjectMonitor monitor) {
        monitor.addProjectListener(sThis.mProjectListener);
        int mask = IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED;
        monitor.addFolderListener(sThis.mFolderListener, mask);
        monitor.addFileListener(sThis.mFileListener, mask);

        CompiledResourcesMonitor.setupMonitor(monitor);
    }

    /**
     * Returns the singleton instance.
     */
    public static ResourceManager getInstance() {
        return sThis;
    }

    /**
     * Adds a new {@link IResourceListener} to be notified of resource changes.
     * @param listener the listener to be added.
     */
    public void addListener(IResourceListener listener) {
        synchronized (mListeners) {
            mListeners.add(listener);
        }
    }

    /**
     * Removes an {@link IResourceListener}, so that it's not notified of resource changes anymore.
     * @param listener the listener to be removed.
     */
    public void removeListener(IResource listener) {
        synchronized (mListeners) {
            mListeners.remove(listener);
        }
    }

    /**
     * Returns the resources of a project.
     * @param project The project
     * @return a ProjectResources object or null if none was found.
     */
    public ProjectResources getProjectResources(IProject project) {
        return mMap.get(project);
    }

    /**
     * Implementation of the {@link IFolderListener} as an internal class so that the methods
     * do not appear in the public API of {@link ResourceManager}.
     */
    private IFolderListener mFolderListener = new IFolderListener() {
        public void folderChanged(IFolder folder, int kind) {
            ProjectResources resources;

            final IProject project = folder.getProject();

            try {
                if (project.hasNature(AndroidConstants.NATURE) == false) {
                    return;
                }
            } catch (CoreException e) {
                // can't get the project nature? return!
                return;
            }

            switch (kind) {
                case IResourceDelta.ADDED:
                    // checks if the folder is under res.
                    IPath path = folder.getFullPath();

                    // the path will be project/res/<something>
                    if (path.segmentCount() == 3) {
                        if (isInResFolder(path)) {
                            // get the project and its resource object.
                            resources = mMap.get(project);

                            // if it doesn't exist, we create it.
                            if (resources == null) {
                                resources = new ProjectResources(false /* isFrameworkRepository */);
                                mMap.put(project, resources);
                            }

                            ResourceFolder newFolder = processFolder(new IFolderWrapper(folder),
                                    resources);
                            if (newFolder != null) {
                                notifyListenerOnFolderChange(project, newFolder, kind);
                            }
                        }
                    }
                    break;
                case IResourceDelta.CHANGED:
                    resources = mMap.get(folder.getProject());
                    if (resources != null) {
                        ResourceFolder resFolder = resources.getResourceFolder(folder);
                        if (resFolder != null) {
                            resFolder.touch();
                            notifyListenerOnFolderChange(project, resFolder, kind);
                        }
                    }
                    break;
                case IResourceDelta.REMOVED:
                    resources = mMap.get(folder.getProject());
                    if (resources != null) {
                        // lets get the folder type
                        ResourceFolderType type = ResourceFolderType.getFolderType(
                                folder.getName());

                        ResourceFolder removedFolder = resources.removeFolder(type, folder);
                        if (removedFolder != null) {
                            notifyListenerOnFolderChange(project, removedFolder, kind);
                        }
                    }
                    break;
            }
        }
    };

    /**
     * Implementation of the {@link IFileListener} as an internal class so that the methods
     * do not appear in the public API of {@link ResourceManager}.
     */
    private IFileListener mFileListener = new IFileListener() {
        /* (non-Javadoc)
         * Sent when a file changed. Depending on the file being changed, and the type of change
         * (ADDED, REMOVED, CHANGED), the file change is processed to update the resource
         * manager data.
         *
         * @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)}
         *
         * @see IFileListener#fileChanged
         */
        public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) {
            ProjectResources resources;

            final IProject project = file.getProject();

            try {
                if (project.hasNature(AndroidConstants.NATURE) == false) {
                    return;
                }
            } catch (CoreException e) {
                // can't get the project nature? return!
                return;
            }

            switch (kind) {
                case IResourceDelta.ADDED:
                    // checks if the file is under res/something.
                    IPath path = file.getFullPath();

                    if (path.segmentCount() == 4) {
                        if (isInResFolder(path)) {
                            // get the project and its resources
                            resources = mMap.get(project);

                            IContainer container = file.getParent();
                            if (container instanceof IFolder && resources != null) {

                                ResourceFolder folder = resources.getResourceFolder(
                                        (IFolder)container);

                                if (folder != null) {
                                    ResourceFile resFile = processFile(
                                            new IFileWrapper(file), folder);
                                    notifyListenerOnFileChange(project, resFile, kind);
                                }
                            }
                        }
                    }
                    break;
                case IResourceDelta.CHANGED:
                    // try to find a matching ResourceFile
                    resources = mMap.get(project);
                    if (resources != null) {
                        IContainer container = file.getParent();
                        if (container instanceof IFolder) {
                            ResourceFolder resFolder = resources.getResourceFolder(
                                    (IFolder)container);

                            // we get the delete on the folder before the file, so it is possible
                            // the associated ResourceFolder doesn't exist anymore.
                            if (resFolder != null) {
                                // get the resourceFile, and touch it.
                                ResourceFile resFile = resFolder.getFile(file);
                                if (resFile != null) {
                                    resFile.touch();
                                    notifyListenerOnFileChange(project, resFile, kind);
                                }
                            }
                        }
                    }
                    break;
                case IResourceDelta.REMOVED:
                    // try to find a matching ResourceFile
                    resources = mMap.get(project);
                    if (resources != null) {
                        IContainer container = file.getParent();
                        if (container instanceof IFolder) {
                            ResourceFolder resFolder = resources.getResourceFolder(
                                    (IFolder)container);

                            // we get the delete on the folder before the file, so it is possible
                            // the associated ResourceFolder doesn't exist anymore.
                            if (resFolder != null) {
                                // remove the file
                                ResourceFile resFile = resFolder.removeFile(file);
                                if (resFile != null) {
                                    notifyListenerOnFileChange(project, resFile, kind);
                                }
                            }
                        }
                    }
                    break;
            }
        }
    };


    /**
     * Implementation of the {@link IProjectListener} as an internal class so that the methods
     * do not appear in the public API of {@link ResourceManager}.
     */
    private IProjectListener mProjectListener = new IProjectListener() {
        public void projectClosed(IProject project) {
            mMap.remove(project);
        }

        public void projectDeleted(IProject project) {
            mMap.remove(project);
        }

        public void projectOpened(IProject project) {
            createProject(project);
        }

        public void projectOpenedWithWorkspace(IProject project) {
            createProject(project);
        }
    };

    /**
     * Returns the {@link ResourceFolder} for the given file or <code>null</code> if none exists.
     */
    public ResourceFolder getResourceFolder(IFile file) {
        IContainer container = file.getParent();
        if (container.getType() == IResource.FOLDER) {
            IFolder parent = (IFolder)container;
            IProject project = file.getProject();

            ProjectResources resources = getProjectResources(project);
            if (resources != null) {
                return resources.getResourceFolder(parent);
            }
        }

        return null;
    }

    /**
     * Returns the {@link ResourceFolder} for the given folder or <code>null</code> if none exists.
     */
    public ResourceFolder getResourceFolder(IFolder folder) {
        IProject project = folder.getProject();

        ProjectResources resources = getProjectResources(project);
        if (resources != null) {
            return resources.getResourceFolder(folder);
        }

        return null;
    }

    /**
     * Loads and returns the resources for a given {@link IAndroidTarget}
     * @param androidTarget the target from which to load the framework resources
     */
    public ProjectResources loadFrameworkResources(IAndroidTarget androidTarget) {
        String osResourcesPath = androidTarget.getPath(IAndroidTarget.RESOURCES);

        File frameworkRes = new File(osResourcesPath);
        if (frameworkRes.isDirectory()) {
            ProjectResources resources = new ProjectResources(true /* isFrameworkRepository */);

            try {
                loadResources(resources, frameworkRes);
                return resources;
            } catch (IOException e) {
                // since we test that folders are folders, and files are files, this shouldn't
                // happen. We can ignore it.
            }
        }

        return null;
    }

    /**
     * Loads the resources from a folder, and fills the given {@link ProjectResources}.
     * <p/>
     * This is mostly a utility method that should not be used to process actual Eclipse projects
     * (Those are loaded with {@link #createProject(IProject)} for new project or
     * {@link #processFolder(IAbstractFolder, ProjectResources)} and
     * {@link #processFile(IAbstractFile, ResourceFolder)} for folder/file modifications)<br>
     * This method will process files/folders with implementations of {@link IAbstractFile} and
     * {@link IAbstractFolder} based on {@link File} instead of {@link IFile} and {@link IFolder}
     * respectively. This is not proper for handling {@link IProject}s.
     * </p>
     * This is used to load the framework resources, or to do load project resources when
     * setting rendering tests.
     *
     *
     * @param resources The {@link ProjectResources} files to load. It is expected that the
     * framework flag has been properly setup. This is filled up with the content of the folder.
     * @param folder The folder to read the resources from. This is the top level resource folder
     * (res/)
     * @throws IOException
     */
    public void loadResources(ProjectResources resources, File folder) throws IOException {
        File[] files = folder.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                ResourceFolder resFolder = processFolder(new FolderWrapper(file),
                        resources);

                if (resFolder != null) {
                    // now we process the content of the folder
                    File[] children = file.listFiles();

                    for (File childRes : children) {
                        if (childRes.isFile()) {
                            processFile(new FileWrapper(childRes), resFolder);
                        }
                    }
                }

            }
        }

        // now that we have loaded the files, we need to force load the resources from them
        resources.loadAll();
    }

    /**
     * Initial project parsing to gather resource info.
     * @param project
     */
    private void createProject(IProject project) {
        if (project.isOpen()) {
            try {
                if (project.hasNature(AndroidConstants.NATURE) == false) {
                    return;
                }
            } catch (CoreException e1) {
                // can't check the nature of the project? ignore it.
                return;
            }

            IFolder resourceFolder = project.getFolder(SdkConstants.FD_RESOURCES);

            ProjectResources projectResources = mMap.get(project);
            if (projectResources == null) {
                projectResources = new ProjectResources(false /* isFrameworkRepository */);
                mMap.put(project, projectResources);
            }

            if (resourceFolder != null && resourceFolder.exists()) {
                try {
                    IResource[] resources = resourceFolder.members();

                    for (IResource res : resources) {
                        if (res.getType() == IResource.FOLDER) {
                            IFolder folder = (IFolder)res;
                            ResourceFolder resFolder = processFolder(new IFolderWrapper(folder),
                                    projectResources);

                            if (resFolder != null) {
                                // now we process the content of the folder
                                IResource[] files = folder.members();

                                for (IResource fileRes : files) {
                                    if (fileRes.getType() == IResource.FILE) {
                                        IFile file = (IFile)fileRes;

                                        processFile(new IFileWrapper(file), resFolder);
                                    }
                                }
                            }
                        }
                    }
                } catch (CoreException e) {
                    // This happens if the project is closed or if the folder doesn't exist.
                    // Since we already test for that, we can ignore this exception.
                }
            }
        }
    }

    /**
     * Creates a {@link FolderConfiguration} matching the folder segments.
     * @param folderSegments The segments of the folder name. The first segments should contain
     * the name of the folder
     * @return a FolderConfiguration object, or null if the folder name isn't valid..
     */
    public FolderConfiguration getConfig(String[] folderSegments) {
        FolderConfiguration config = new FolderConfiguration();

        // we are going to loop through the segments, and match them with the first
        // available qualifier. If the segment doesn't match we try with the next qualifier.
        // Because the order of the qualifier is fixed, we do not reset the first qualifier
        // after each sucessful segment.
        // If we run out of qualifier before processing all the segments, we fail.

        int qualifierIndex = 0;
        int qualifierCount = mQualifiers.length;

        for (int i = 1 ; i < folderSegments.length; i++) {
            String seg = folderSegments[i];
            if (seg.length() > 0) {
                while (qualifierIndex < qualifierCount &&
                        mQualifiers[qualifierIndex].checkAndSet(seg, config) == false) {
                    qualifierIndex++;
                }

                // if we reached the end of the qualifier we didn't find a matching qualifier.
                if (qualifierIndex == qualifierCount) {
                    return null;
                }

            } else {
                return null;
            }
        }

        return config;
    }

    /**
     * Processes a folder and adds it to the list of the project resources.
     * @param folder the folder to process
     * @param project the folder's project.
     * @return the ConfiguredFolder created from this folder, or null if the process failed.
     */
    private ResourceFolder processFolder(IAbstractFolder folder, ProjectResources project) {
        // split the name of the folder in segments.
        String[] folderSegments = folder.getName().split(FolderConfiguration.QUALIFIER_SEP);

        // get the enum for the resource type.
        ResourceFolderType type = ResourceFolderType.getTypeByName(folderSegments[0]);

        if (type != null) {
            // get the folder configuration.
            FolderConfiguration config = getConfig(folderSegments);

            if (config != null) {
                ResourceFolder configuredFolder = project.add(type, config, folder);

                return configuredFolder;
            }
        }

        return null;
    }

    /**
     * Processes a file and adds it to its parent folder resource.
     * @param file the underlying resource file.
     * @param folder the parent of the resource file.
     * @return the {@link ResourceFile} that was created.
     */
    private ResourceFile processFile(IAbstractFile file, ResourceFolder folder) {
        // get the type of the folder
        ResourceFolderType type = folder.getType();

        // look for this file if it's already been created
        ResourceFile resFile = folder.getFile(file);

        if (resFile != null) {
            // invalidate the file
            resFile.touch();
        } else {
            // create a ResourceFile for it.

            // check if that's a single or multi resource type folder. For now we define this by
            // the number of possible resource type output by files in the folder. This does
            // not make the difference between several resource types from a single file or
            // the ability to have 2 files in the same folder generating 2 different types of
            // resource. The former is handled by MultiResourceFile properly while we don't
            // handle the latter. If we were to add this behavior we'd have to change this call.
            ResourceType[] types = FolderTypeRelationship.getRelatedResourceTypes(type);

            if (types.length == 1) {
                resFile = new SingleResourceFile(file, folder);
            } else {
                resFile = new MultiResourceFile(file, folder);
            }

            // add it to the folder
            folder.addFile(resFile);
        }

        return resFile;
    }

    /**
     * Returns true if the path is under /project/res/
     * @param path a workspace relative path
     * @return true if the path is under /project res/
     */
    private boolean isInResFolder(IPath path) {
        return SdkConstants.FD_RESOURCES.equalsIgnoreCase(path.segment(1));
    }

    private void notifyListenerOnFolderChange(IProject project, ResourceFolder folder,
            int eventType) {
        synchronized (mListeners) {
            for (IResourceListener listener : mListeners) {
                try {
                    listener.folderChanged(project, folder, eventType);
                } catch (Throwable t) {
                    AdtPlugin.log(t,
                            "Failed to execute ResourceManager.IResouceListener.folderChanged()"); //$NON-NLS-1$
                }
            }
        }
    }

    private void notifyListenerOnFileChange(IProject project, ResourceFile file, int eventType) {
        synchronized (mListeners) {
            for (IResourceListener listener : mListeners) {
                try {
                    listener.fileChanged(project, file, eventType);
                } catch (Throwable t) {
                    AdtPlugin.log(t,
                            "Failed to execute ResourceManager.IResouceListener.fileChanged()"); //$NON-NLS-1$
                }
            }
        }
    }

    /**
     * Private constructor to enforce singleton design.
     */
    private ResourceManager() {
        // get the default qualifiers.
        FolderConfiguration defaultConfig = new FolderConfiguration();
        defaultConfig.createDefault();
        mQualifiers = defaultConfig.getQualifiers();
    }
}
