/*
 * Copyright (C) 2011 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.build;

import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.IJavaProject;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Base class to handle generated java code.
 *
 * It provides management for modified source file list, deleted source file list, reconciliation
 * of previous lists, storing the current state of the build.
 *
 */
public abstract class SourceProcessor {

    public final static int COMPILE_STATUS_NONE = 0;
    public final static int COMPILE_STATUS_CODE = 0x1;
    public final static int COMPILE_STATUS_RES = 0x2;

    /** List of all source files, their dependencies, and their output. */
    private final Map<IFile, SourceFileData> mFiles = new HashMap<IFile, SourceFileData>();

    private final IJavaProject mJavaProject;
    private final IFolder mGenFolder;
    private final SourceChangeHandler mDeltaVisitor;

    /** List of source files pending compilation at the next build */
    private final List<IFile> mToCompile = new ArrayList<IFile>();

    /** List of removed source files pending cleaning at the next build. */
    private final List<IFile> mRemoved = new ArrayList<IFile>();

    private int mLastCompilationStatus = COMPILE_STATUS_NONE;

    /**
     * Quotes a path inside "". If the platform is not windows, the path is returned as is.
     * @param path the path to quote
     * @return the quoted path.
     */
    public static String quote(String path) {
        if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
            return "\"" + path + "\"";
        }

        return path;
    }

    protected SourceProcessor(IJavaProject javaProject, IFolder genFolder,
            SourceChangeHandler deltaVisitor) {
        mJavaProject = javaProject;
        mGenFolder = genFolder;
        mDeltaVisitor = deltaVisitor;

        mDeltaVisitor.init(this);

        IProject project = javaProject.getProject();

        // get all the source files
        buildSourceFileList();

        // load the known dependencies
        loadOutputAndDependencies();

        boolean mustCompile = loadState(project);

        // if we stored that we have to compile some files, we build the list that will compile them
        // all. For now we have to reuse the full list since we don't know which files needed
        // compilation.
        if (mustCompile) {
            mToCompile.addAll(mFiles.keySet());
        }
    }

    protected SourceProcessor(IJavaProject javaProject, IFolder genFolder) {
        this(javaProject, genFolder, new SourceChangeHandler());
    }


    /**
     * Returns whether the given file is an output of this processor by return the source
     * file that generated it.
     * @param file the file to test.
     * @return the source file that generated the given file or null.
     */
    IFile isOutput(IFile file) {
        for (SourceFileData data : mFiles.values()) {
            if (data.generated(file)) {
                return data.getSourceFile();
            }
        }

        return null;
    }

    /**
     * Returns whether the given file is a dependency for other files by returning a list
     * of file depending on the given file.
     * @param file the file to test.
     * @return a list of files that depend on the given file or an empty list if there
     *    are no matches.
     */
    List<IFile> isDependency(IFile file) {
        ArrayList<IFile> files = new ArrayList<IFile>();
        for (SourceFileData data : mFiles.values()) {
            if (data.dependsOn(file)) {
                files.add(data.getSourceFile());
            }
        }

        return files;
    }

    void addData(SourceFileData data) {
        mFiles.put(data.getSourceFile(), data);
    }

    SourceFileData getFileData(IFile file) {
        return mFiles.get(file);
    }

    Collection<SourceFileData> getAllFileData() {
        return mFiles.values();
    }

    public final SourceChangeHandler getChangeHandler() {
        return mDeltaVisitor;
    }

    final IJavaProject getJavaProject() {
        return mJavaProject;
    }

    final IFolder getGenFolder() {
        return mGenFolder;
    }

    final List<IFile> getToCompile() {
        return mToCompile;
    }

    final List<IFile> getRemovedFile() {
        return mRemoved;
    }

    final void addFileToCompile(IFile file) {
        mToCompile.add(file);
    }

    public final void prepareFullBuild(IProject project) {
        mDeltaVisitor.reset();

        mToCompile.clear();
        mRemoved.clear();

        // get all the source files
        buildSourceFileList();

        mToCompile.addAll(mFiles.keySet());

        saveState(project);
    }

    public final void doneVisiting(IProject project) {
        // merge the previous file modification lists and the new one.
        mergeFileModifications(mDeltaVisitor);

        mDeltaVisitor.reset();

        saveState(project);
    }

    /**
     * Returns the extension of the source files handled by this processor.
     * @return
     */
    protected abstract String getExtension();

    protected abstract String getSavePropertyName();

    /**
     * Compiles the source files and return a status bitmask of the type of file that was generated.
     *
     */
    public final int compileFiles(BaseBuilder builder,
            IProject project, IAndroidTarget projectTarget, int minSdkVersion,
            List<IPath> sourceFolders, IProgressMonitor monitor) throws CoreException {

        mLastCompilationStatus = COMPILE_STATUS_NONE;

        if (mToCompile.size() == 0 && mRemoved.size() == 0) {
            return mLastCompilationStatus;
        }

        // if a source file is being removed before we managed to compile it, it'll be in
        // both list. We *need* to remove it from the compile list or it'll never go away.
        for (IFile sourceFile : mRemoved) {
            int pos = mToCompile.indexOf(sourceFile);
            if (pos != -1) {
                mToCompile.remove(pos);
            }
        }

        // list of files that have failed compilation.
        List<IFile> stillNeedCompilation = new ArrayList<IFile>();

        doCompileFiles(mToCompile, builder, project, projectTarget, minSdkVersion, sourceFolders,
                stillNeedCompilation, monitor);

        mToCompile.clear();
        mToCompile.addAll(stillNeedCompilation);

        // Remove the files created from source files that have been removed.
        for (IFile sourceFile : mRemoved) {
            // look if we already know the output
            SourceFileData data = getFileData(sourceFile);
            if (data != null) {
                doRemoveFiles(data);
            }
        }

        // remove the associated file data.
        for (IFile removedFile : mRemoved) {
            mFiles.remove(removedFile);
        }

        mRemoved.clear();

        // store the build state. If there are any files that failed to compile, we will
        // force a full aidl compile on the next project open. (unless a full compilation succeed
        // before the project is closed/re-opened.)
        saveState(project);

        return mLastCompilationStatus;
    }

    protected abstract void doCompileFiles(
            List<IFile> filesToCompile, BaseBuilder builder,
            IProject project, IAndroidTarget projectTarget, int targetApi,
            List<IPath> sourceFolders, List<IFile> notCompiledOut, IProgressMonitor monitor)
            throws CoreException;

    /**
     * Adds a compilation status. It can be any of (in combination too):
     * <p/>
     * {@link #COMPILE_STATUS_CODE} means this processor created source code files.
     * {@link #COMPILE_STATUS_RES} means this process created resources.
     */
    protected void setCompilationStatus(int status) {
        mLastCompilationStatus |= status;
    }

    protected void doRemoveFiles(SourceFileData data) throws CoreException {
        List<IFile> outputFiles = data.getOutputFiles();
        for (IFile outputFile : outputFiles) {
            if (outputFile.exists()) {
                outputFile.getLocation().toFile().delete();
            }
        }
    }

    public final boolean loadState(IProject project) {
        return ProjectHelper.loadBooleanProperty(project, getSavePropertyName(),
                true /*defaultValue*/);
    }

    public final void saveState(IProject project) {
        // TODO: Optimize by saving only the files that need compilation
        ProjectHelper.saveStringProperty(project, getSavePropertyName(),
                Boolean.toString(mToCompile.size() > 0));
    }

    protected abstract void loadOutputAndDependencies();


    protected IPath getSourceFolderFor(IFile file) {
        // find the source folder for the class so that we can infer the package from the
        // difference between the file and its source folder.
        List<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(getJavaProject());
        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();

        for (IPath sourceFolderPath : sourceFolders) {
            IFolder sourceFolder = root.getFolder(sourceFolderPath);
            // we don't look in the 'gen' source folder as there will be no source in there.
            if (sourceFolder.exists() && sourceFolder.equals(getGenFolder()) == false) {
                // look for the source file parent, until we find this source folder.
                IResource parent = file;
                while ((parent = parent.getParent()) != null) {
                    if (parent.equals(sourceFolder)) {
                        return sourceFolderPath;
                    }
                }
            }
        }

        return null;
    }

    /**
     * Goes through the build paths and fills the list of files to compile.
     *
     * @param project The project.
     * @param sourceFolderPathList The list of source folder paths.
     */
    private final void buildSourceFileList() {
        mFiles.clear();

        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        List<IPath> sourceFolderPathList = BaseProjectHelper.getSourceClasspaths(mJavaProject);

        for (IPath sourceFolderPath : sourceFolderPathList) {
            IFolder sourceFolder = root.getFolder(sourceFolderPath);
            // we don't look in the 'gen' source folder as there will be no source in there.
            if (sourceFolder.exists() && sourceFolder.equals(getGenFolder()) == false) {
                scanFolderForSourceFiles(sourceFolder, sourceFolder);
            }
        }
    }

    /**
     * Scans a folder and fills the list of files to compile.
     * @param sourceFolder the root source folder.
     * @param folder The folder to scan.
     */
    private void scanFolderForSourceFiles(IFolder sourceFolder, IFolder folder) {
        try {
            IResource[] members = folder.members();
            for (IResource r : members) {
                // get the type of the resource
               switch (r.getType()) {
                   case IResource.FILE:
                       // if this a file, check that the file actually exist
                       // and that it's the type of of file that's used in this processor
                       if (r.exists() &&
                               getExtension().equalsIgnoreCase(r.getFileExtension())) {
                           mFiles.put((IFile) r, new SourceFileData((IFile) r));
                       }
                       break;
                   case IResource.FOLDER:
                       // recursively go through children
                       scanFolderForSourceFiles(sourceFolder, (IFolder)r);
                       break;
                   default:
                       // this would mean it's a project or the workspace root
                       // which is unlikely to happen. we do nothing
                       break;
               }
            }
        } catch (CoreException e) {
            // Couldn't get the members list for some reason. Just return.
        }
    }


    /**
     * Merge the current list of source file to compile/remove with the one coming from the
     * delta visitor
     * @param visitor the delta visitor.
     */
    private void mergeFileModifications(SourceChangeHandler visitor) {
        Set<IFile> toRemove = visitor.getRemovedFiles();
        Set<IFile> toCompile = visitor.getFilesToCompile();

        // loop through the new toRemove list, and add it to the old one,
        // plus remove any file that was still to compile and that are now
        // removed
        for (IFile r : toRemove) {
            if (mRemoved.indexOf(r) == -1) {
                mRemoved.add(r);
            }

            int index = mToCompile.indexOf(r);
            if (index != -1) {
                mToCompile.remove(index);
            }
        }

        // now loop through the new files to compile and add it to the list.
        // Also look for them in the remove list, this would mean that they
        // were removed, then added back, and we shouldn't remove them, just
        // recompile them.
        for (IFile r : toCompile) {
            if (mToCompile.indexOf(r) == -1) {
                mToCompile.add(r);
            }

            int index = mRemoved.indexOf(r);
            if (index != -1) {
                mRemoved.remove(index);
            }
        }
    }
}
