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

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AndroidConstants;
import com.android.ide.eclipse.adt.internal.build.BaseBuilder.BaseDeltaVisitor;
import com.android.ide.eclipse.adt.internal.build.PreCompilerBuilder.AidlData;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
import com.android.ide.eclipse.adt.io.IFileWrapper;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.xml.ManifestData;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
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 java.util.ArrayList;

/**
 * Resource Delta visitor for the pre-compiler.
 * <p/>This delta visitor only cares about files that are the source or the result of actions of the
 * {@link PreCompilerBuilder}:
 * <ul><li>R.java/Manifest.java generated by compiling the resources</li>
 * <li>Any Java files generated by <code>aidl</code></li></ul>.
 *
 * Therefore it looks for the following:
 * <ul><li>Any modification in the resource folder</li>
 * <li>Removed files from the source folder receiving generated Java files</li>
 * <li>Any modification to aidl files.</li>
 *
 */
class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements
        IResourceDeltaVisitor {

    private enum AidlType {
        UNKNOWN, INTERFACE, PARCELABLE;
    }

    // See comment in #getAidlType()
//    private final static Pattern sParcelablePattern = Pattern.compile(
//            "^\\s*parcelable\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*;\\s*$");
//
//    private final static Pattern sInterfacePattern = Pattern.compile(
//            "^\\s*interface\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?:\\{.*)?$");

    // Result fields.
    /**
     * Compile flag. This is set to true if one of the changed/added/removed
     * file is a resource file. Upon visiting all the delta resources, if
     * this flag is true, then we know we'll have to compile the resources
     * into R.java
     */
    private boolean mCompileResources = false;

    /**
     * Aidl force recompilation flag. If true, we'll attempt to recompile all aidl files.
     */
    private boolean mForceAidlCompile = false;

    /** List of .aidl files found that are modified or new. */
    private final ArrayList<AidlData> mAidlToCompile = new ArrayList<AidlData>();

    /** List of .aidl files that have been removed. */
    private final ArrayList<AidlData> mAidlToRemove = new ArrayList<AidlData>();

    /** Manifest check/parsing flag. */
    private boolean mCheckedManifestXml = false;

    /** Application Package, gathered from the parsing of the manifest */
    private String mJavaPackage = null;
    /** minSDKVersion attribute value, gathered from the parsing of the manifest */
    private String mMinSdkVersion = null;

    // Internal usage fields.
    /**
     * In Resource folder flag. This allows us to know if we're in the
     * resource folder.
     */
    private boolean mInRes = false;

    /**
     * Current Source folder. This allows us to know if we're in a source
     * folder, and which folder.
     */
    private IFolder mSourceFolder = null;

    /** List of source folders. */
    private ArrayList<IPath> mSourceFolders;
    private boolean mIsGenSourceFolder = false;

    private IWorkspaceRoot mRoot;


    public PreCompilerDeltaVisitor(BaseBuilder builder, ArrayList<IPath> sourceFolders) {
        super(builder);
        mSourceFolders = sourceFolders;
        mRoot = ResourcesPlugin.getWorkspace().getRoot();
    }

    public boolean getCompileResources() {
        return mCompileResources;
    }

    public boolean getForceAidlCompile() {
        return mForceAidlCompile;
    }

    public ArrayList<AidlData> getAidlToCompile() {
        return mAidlToCompile;
    }

    public ArrayList<AidlData> getAidlToRemove() {
        return mAidlToRemove;
    }

    /**
     * Returns whether the manifest file was parsed/checked for error during the resource delta
     * visiting.
     */
    public boolean getCheckedManifestXml() {
        return mCheckedManifestXml;
    }

    /**
     * Returns the manifest package if the manifest was checked/parsed.
     * <p/>
     * This can return null in two cases:
     * <ul>
     * <li>The manifest was not part of the resource change delta, and the manifest was
     * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
     * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
     * but the package declaration is missing</li>
     * </ul>
     * @return the manifest package or null.
     */
    public String getManifestPackage() {
        return mJavaPackage;
    }

    /**
     * Returns the minSDkVersion attribute from the manifest if it was checked/parsed.
     * <p/>
     * This can return null in two cases:
     * <ul>
     * <li>The manifest was not part of the resource change delta, and the manifest was
     * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
     * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
     * but the package declaration is missing</li>
     * </ul>
     * @return the minSdkVersion or null.
     */
    public String getMinSdkVersion() {
        return mMinSdkVersion;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.eclipse.core.resources.IResourceDeltaVisitor
     *      #visit(org.eclipse.core.resources.IResourceDelta)
     */
    public boolean visit(IResourceDelta delta) throws CoreException {
        // we are only going to look for changes in res/, source folders and in
        // AndroidManifest.xml since the delta visitor goes through the main
        // folder before its children we can check when the path segment
        // count is 2 (format will be /$Project/folder) and make sure we are
        // processing res/, source folders or AndroidManifest.xml

        IResource resource = delta.getResource();
        IPath path = resource.getFullPath();
        String[] segments = path.segments();

        // since the delta visitor also visits the root we return true if
        // segments.length = 1
        if (segments.length == 1) {
            // this is always the Android project since we call
            // Builder#getDelta(IProject) on the project itself.
            return true;
        } else if (segments.length == 2) {
            // if we are at an item directly under the root directory,
            // then we are not yet in a source or resource folder
            mInRes = false;
            mSourceFolder = null;

            if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) {
                // this is the resource folder that was modified. we want to
                // see its content.

                // since we're going to visit its children next, we set the
                // flag
                mInRes = true;
                mSourceFolder = null;
                return true;
            } else if (SdkConstants.FN_ANDROID_MANIFEST_XML.equalsIgnoreCase(segments[1])) {
                // any change in the manifest could trigger a new R.java
                // class, so we don't need to check the delta kind
                if (delta.getKind() != IResourceDelta.REMOVED) {
                    // clean the error markers on the file.
                    IFile manifestFile = (IFile)resource;

                    if (manifestFile.exists()) {
                        manifestFile.deleteMarkers(AndroidConstants.MARKER_XML, true,
                                IResource.DEPTH_ZERO);
                        manifestFile.deleteMarkers(AndroidConstants.MARKER_ANDROID, true,
                                IResource.DEPTH_ZERO);
                    }

                    // parse the manifest for data and error
                    ManifestData manifestData = AndroidManifestHelper.parse(
                            new IFileWrapper(manifestFile), true /*gatherData*/, this);

                    if (manifestData != null) {
                        mJavaPackage = manifestData.getPackage();
                        mMinSdkVersion = manifestData.getMinSdkVersionString();
                    }

                    mCheckedManifestXml = true;
                }
                mCompileResources = true;

                // we don't want to go to the children, not like they are
                // any for this resource anyway.
                return false;
            }
        }

        // at this point we can either be in the source folder or in the
        // resource folder or in a different folder that contains a source
        // folder.
        // This is due to not all source folder being src/. Some could be
        // something/somethingelse/src/

        // so first we test if we already know we are in a source or
        // resource folder.

        if (mSourceFolder != null) {
            // if we are in the res folder, we are looking for the following changes:
            // - added/removed/modified aidl files.
            // - missing R.java file

            // if the resource is a folder, we just go straight to the children
            if (resource.getType() == IResource.FOLDER) {
                return true;
            }

            if (resource.getType() != IResource.FILE) {
                return false;
            }
            IFile file = (IFile)resource;

            // get the modification kind
            int kind = delta.getKind();

            // we process normal source folder and the 'gen' source folder differently.
            if (mIsGenSourceFolder) {
                // this is the generated java file source folder.
                // - if R.java/Manifest.java are removed/modified, we recompile the resources
                // - if aidl files are removed/modified, we recompile them.

                boolean outputWarning = false;

                String fileName = resource.getName();

                // Special case of R.java/Manifest.java.
                if (AndroidConstants.FN_RESOURCE_CLASS.equals(fileName) ||
                        AndroidConstants.FN_MANIFEST_CLASS.equals(fileName)) {
                    // if it was removed, there's a possibility that it was removed due to a
                    // package change, or an aidl that was removed, but the only thing
                    // that will happen is that we'll have an extra build. Not much of a problem.
                    mCompileResources = true;

                    // we want a warning
                    outputWarning = true;
                } else {
                    // this has to be a Java file created from an aidl file.
                    // Look for the source aidl file in all the source folders.
                    String aidlFileName = fileName.replaceAll(AndroidConstants.RE_JAVA_EXT,
                            AndroidConstants.DOT_AIDL);

                    for (IPath sourceFolderPath : mSourceFolders) {
                        // do not search in the current source folder as it is the 'gen' folder.
                        if (sourceFolderPath.equals(mSourceFolder.getFullPath())) {
                            continue;
                        }

                        IFolder sourceFolder = getFolder(sourceFolderPath);
                        if (sourceFolder != null) {
                            // go recursively, segment by segment.
                            // index starts at 2 (0 is project, 1 is 'gen'
                            IFile sourceFile = findFile(sourceFolder, segments, 2, aidlFileName);

                            if (sourceFile != null) {
                                // found the source. add it to the list of files to compile
                                mAidlToCompile.add(new AidlData(sourceFolder, sourceFile));
                                outputWarning = true;
                                break;
                            }
                        }
                    }
                }

                if (outputWarning) {
                    if (kind == IResourceDelta.REMOVED) {
                        // We pring an error just so that it's red, but it's just a warning really.
                        String msg = String.format(Messages.s_Removed_Recreating_s, fileName);
                        AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg);
                    } else if (kind == IResourceDelta.CHANGED) {
                        // the file was modified manually! we can't allow it.
                        String msg = String.format(Messages.s_Modified_Manually_Recreating_s,
                                fileName);
                        AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg);
                    }
                }
            } else {
                // this is another source folder.
                // We only care about aidl files being added/modified/removed.

                // get the extension of the resource
                String ext = resource.getFileExtension();
                if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) {
                    // first check whether it's a regular file or a parcelable.
                    AidlType type = getAidlType(file);

                    if (type == AidlType.INTERFACE) {
                        if (kind == IResourceDelta.REMOVED) {
                            // we'll have to remove the generated file.
                            mAidlToRemove.add(new AidlData(mSourceFolder, file));
                        } else if (mForceAidlCompile == false) {
                            // add the aidl file to the list of file to (re)compile
                            mAidlToCompile.add(new AidlData(mSourceFolder, file));
                        }
                    } else {
                        // force recompilations of all Aidl Files.
                        mForceAidlCompile = true;
                        mAidlToCompile.clear();
                    }
                }
            }

            // no children.
            return false;
        } else if (mInRes) {
            // if we are in the res folder, we are looking for the following
            // changes:
            // - added/removed/modified xml files.
            // - added/removed files of any other type

            // if the resource is a folder, we just go straight to the
            // children
            if (resource.getType() == IResource.FOLDER) {
                return true;
            }

            // get the extension of the resource
            String ext = resource.getFileExtension();
            int kind = delta.getKind();

            String p = resource.getProjectRelativePath().toString();
            String message = null;
            switch (kind) {
                case IResourceDelta.CHANGED:
                    // display verbose message
                    message = String.format(Messages.s_Modified_Recreating_s, p,
                            AndroidConstants.FN_RESOURCE_CLASS);
                    break;
                case IResourceDelta.ADDED:
                    // display verbose message
                    message = String.format(Messages.Added_s_s_Needs_Updating, p,
                            AndroidConstants.FN_RESOURCE_CLASS);
                    break;
                case IResourceDelta.REMOVED:
                    // display verbose message
                    message = String.format(Messages.s_Removed_s_Needs_Updating, p,
                            AndroidConstants.FN_RESOURCE_CLASS);
                    break;
            }
            if (message != null) {
                AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE,
                        mBuilder.getProject(), message);
            }

            if (AndroidConstants.EXT_XML.equalsIgnoreCase(ext)) {
                if (kind != IResourceDelta.REMOVED) {
                    // check xml Validity
                    mBuilder.checkXML(resource, this);
                }

                // if we are going through this resource, it was modified
                // somehow.
                // we don't care if it was an added/removed/changed event
                mCompileResources = true;
                return false;
            } else {
                // this is a non xml resource.
                if (kind == IResourceDelta.ADDED
                        || kind == IResourceDelta.REMOVED) {
                    mCompileResources = true;
                    return false;
                }
            }
        } else if (resource instanceof IFolder) {
            // in this case we may be inside a folder that contains a source
            // folder, go through the list of known source folders

            for (IPath sourceFolderPath : mSourceFolders) {
                // first check if they match exactly.
                if (sourceFolderPath.equals(path)) {
                    // this is a source folder!
                    mInRes = false;
                    mSourceFolder = getFolder(sourceFolderPath); // all non null due to test above
                    mIsGenSourceFolder = path.segmentCount() == 2 &&
                            path.segment(1).equals(SdkConstants.FD_GEN_SOURCES);
                    return true;
                }

                // check if we are on the way to a source folder.
                int count = sourceFolderPath.matchingFirstSegments(path);
                if (count == path.segmentCount()) {
                    mInRes = false;
                    return true;
                }
            }

            // if we're here, we are visiting another folder
            // like /$Project/bin/ for instance (we get notified for changes
            // in .class!)
            // This could also be another source folder and we have found
            // R.java in a previous source folder
            // We don't want to visit its children
            return false;
        }

        return false;
    }

    /**
     * Searches for and return a file in a folder. The file is defined by its segments, and a new
     * name (replacing the last segment).
     * @param folder the folder we are searching
     * @param segments the segments of the file to search.
     * @param index the index of the current segment we are looking for
     * @param filename the new name to replace the last segment.
     * @return the {@link IFile} representing the searched file, or null if not found
     */
    private IFile findFile(IFolder folder, String[] segments, int index, String filename) {
        boolean lastSegment = index == segments.length - 1;
        IResource resource = folder.findMember(lastSegment ? filename : segments[index]);
        if (resource != null && resource.exists()) {
            if (lastSegment) {
                if (resource.getType() == IResource.FILE) {
                    return (IFile)resource;
                }
            } else {
                if (resource.getType() == IResource.FOLDER) {
                    return findFile((IFolder)resource, segments, index+1, filename);
                }
            }
        }
        return null;
    }

    /**
     * Returns a handle to the folder identified by the given path in this container.
     * <p/>The different with {@link IContainer#getFolder(IPath)} is that this returns a non
     * null object only if the resource actually exists and is a folder (and not a file)
     * @param path the path of the folder to return.
     * @return a handle to the folder if it exists, or null otherwise.
     */
    private IFolder getFolder(IPath path) {
        IResource resource = mRoot.findMember(path);
        if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) {
            return (IFolder)resource;
        }

        return null;
    }

    /**
     * Returns the type of the aidl file. Aidl files can either declare interfaces, or declare
     * parcelables. This method will attempt to parse the file and return the type. If the type
     * cannot be determined, then it will return {@link AidlType#UNKNOWN}.
     * @param file The aidl file
     * @return the type of the aidl.
     * @throws CoreException
     */
    private AidlType getAidlType(IFile file) throws CoreException {
        // At this time, parsing isn't available, so we return UNKNOWN. This will force
        // a recompilation of all aidl file as soon as one is changed.
        return AidlType.UNKNOWN;

        // TODO: properly parse aidl file to determine type and generate dependency graphs.
//
//        String className = file.getName().substring(0,
//                file.getName().length() - AndroidConstants.DOT_AIDL.length());
//
//        InputStream input = file.getContents(true /* force*/);
//        try {
//            BufferedReader reader = new BufferedReader(new InputStreamReader(input));
//            String line;
//            while ((line = reader.readLine()) != null) {
//                if (line.length() == 0) {
//                    continue;
//                }
//
//                Matcher m = sParcelablePattern.matcher(line);
//                if (m.matches() && m.group(1).equals(className)) {
//                    return AidlType.PARCELABLE;
//                }
//
//                m = sInterfacePattern.matcher(line);
//                if (m.matches() && m.group(1).equals(className)) {
//                    return AidlType.INTERFACE;
//                }
//            }
//        } catch (IOException e) {
//            throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
//                    "Error parsing aidl file", e));
//        } finally {
//            try {
//                input.close();
//            } catch (IOException e) {
//                throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
//                        "Error parsing aidl file", e));
//            }
//        }
//
//        return AidlType.UNKNOWN;
    }
}
